mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 11:49:25 +00:00
Asset marketplace
This commit is contained in:
@@ -40,13 +40,26 @@ CREATE TABLE IF NOT EXISTS audio_assets (
|
||||
CREATE TABLE IF NOT EXISTS script_assets (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
is_public BOOLEAN,
|
||||
media_type TEXT,
|
||||
original_media_type TEXT
|
||||
original_media_type TEXT,
|
||||
logo_file_id TEXT,
|
||||
source_file_id TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS script_asset_files (
|
||||
id TEXT PRIMARY KEY,
|
||||
broadcaster TEXT NOT NULL,
|
||||
media_type TEXT,
|
||||
original_media_type TEXT,
|
||||
asset_type TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS script_asset_attachments (
|
||||
id TEXT PRIMARY KEY,
|
||||
script_asset_id TEXT NOT NULL,
|
||||
file_id TEXT,
|
||||
name TEXT NOT NULL,
|
||||
media_type TEXT,
|
||||
original_media_type TEXT,
|
||||
|
||||
@@ -21,6 +21,21 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal .modal-inner.small {
|
||||
width: 460px;
|
||||
}
|
||||
|
||||
.modal .modal-inner.wide {
|
||||
width: 960px;
|
||||
}
|
||||
|
||||
.modal .modal-inner .modal-header-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.modal .modal-inner form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -38,6 +53,17 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal .modal-inner .form-actions.split {
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal .modal-inner .checkbox-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal .modal-inner textarea {
|
||||
max-width: 100%;
|
||||
resize: vertical;
|
||||
@@ -54,6 +80,7 @@
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.modal .modal-inner .attachment-actions .file-input-trigger.small {
|
||||
@@ -100,3 +127,79 @@
|
||||
color: rgba(226, 232, 240, 0.7);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.modal .modal-inner .logo-preview {
|
||||
margin-top: 8px;
|
||||
min-height: 60px;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed rgba(148, 163, 184, 0.35);
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal .modal-inner .logo-preview img {
|
||||
max-height: 80px;
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.modal .modal-inner .marketplace-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.modal .modal-inner .marketplace-card {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 1fr auto;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.3);
|
||||
border-radius: 10px;
|
||||
background-color: rgba(15, 23, 42, 0.6);
|
||||
}
|
||||
|
||||
.modal .modal-inner .marketplace-logo {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 12px;
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
object-fit: cover;
|
||||
border: 1px solid rgba(148, 163, 184, 0.3);
|
||||
}
|
||||
|
||||
.modal .modal-inner .marketplace-logo.placeholder {
|
||||
color: rgba(226, 232, 240, 0.7);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.modal .modal-inner .marketplace-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.modal .modal-inner .marketplace-content p {
|
||||
margin: 0;
|
||||
color: rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.modal .modal-inner .marketplace-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal .modal-inner .marketplace-empty,
|
||||
.modal .modal-inner .marketplace-loading {
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border: 1px dashed rgba(148, 163, 184, 0.3);
|
||||
color: rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createCustomAssetModal } from "./customAssets.js";
|
||||
let adminConsole;
|
||||
const customAssetModal = createCustomAssetModal({
|
||||
broadcaster,
|
||||
adminChannels: ADMIN_CHANNELS,
|
||||
showToast: globalThis.showToast,
|
||||
onAssetSaved: (asset) => adminConsole?.handleCustomAssetSaved(asset),
|
||||
});
|
||||
|
||||
@@ -111,8 +111,8 @@ export function createAdminConsole({
|
||||
});
|
||||
}
|
||||
const customAssetButton = document.getElementById("custom-asset-button");
|
||||
if (customAssetButton && customAssetModal?.openNew) {
|
||||
customAssetButton.addEventListener("click", () => customAssetModal.openNew());
|
||||
if (customAssetButton && customAssetModal?.openLauncher) {
|
||||
customAssetButton.addEventListener("click", () => customAssetModal.openLauncher());
|
||||
}
|
||||
globalThis.addEventListener("resize", () => {
|
||||
resizeCanvas();
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
export function createCustomAssetModal({ broadcaster, showToast = globalThis.showToast, onAssetSaved }) {
|
||||
export function createCustomAssetModal({
|
||||
broadcaster,
|
||||
adminChannels = [],
|
||||
showToast = globalThis.showToast,
|
||||
onAssetSaved,
|
||||
}) {
|
||||
const launchModal = document.getElementById("custom-asset-launch-modal");
|
||||
const launchNewButton = document.getElementById("custom-asset-launch-new");
|
||||
const launchMarketplaceButton = document.getElementById("custom-asset-launch-marketplace");
|
||||
const marketplaceModal = document.getElementById("custom-asset-marketplace-modal");
|
||||
const marketplaceCloseButton = document.getElementById("custom-asset-marketplace-close");
|
||||
const marketplaceSearchInput = document.getElementById("custom-asset-marketplace-search");
|
||||
const marketplaceList = document.getElementById("custom-asset-marketplace-list");
|
||||
const marketplaceChannelSelect = document.getElementById("custom-asset-marketplace-channel");
|
||||
const assetModal = document.getElementById("custom-asset-modal");
|
||||
const userNameInput = document.getElementById("custom-asset-name");
|
||||
const descriptionInput = document.getElementById("custom-asset-description");
|
||||
const publicCheckbox = document.getElementById("custom-asset-public");
|
||||
const logoInput = document.getElementById("custom-asset-logo-file");
|
||||
const logoPreview = document.getElementById("custom-asset-logo-preview");
|
||||
const logoClearButton = document.getElementById("custom-asset-logo-clear");
|
||||
const userSourceTextArea = document.getElementById("custom-asset-code");
|
||||
const formErrorWrapper = document.getElementById("custom-asset-error");
|
||||
const jsErrorTitle = document.getElementById("js-error-title");
|
||||
@@ -11,7 +29,10 @@ export function createCustomAssetModal({ broadcaster, showToast = globalThis.sho
|
||||
const attachmentList = document.getElementById("custom-asset-attachment-list");
|
||||
const attachmentHint = document.getElementById("custom-asset-attachment-hint");
|
||||
let currentAssetId = null;
|
||||
let pendingLogoFile = null;
|
||||
let logoRemoved = false;
|
||||
let attachmentState = [];
|
||||
let marketplaceEntries = [];
|
||||
|
||||
const resetErrors = () => {
|
||||
if (formErrorWrapper) {
|
||||
@@ -25,6 +46,30 @@ export function createCustomAssetModal({ broadcaster, showToast = globalThis.sho
|
||||
}
|
||||
};
|
||||
|
||||
const openLaunchModal = () => {
|
||||
launchModal?.classList.remove("hidden");
|
||||
};
|
||||
|
||||
const closeLaunchModal = () => {
|
||||
launchModal?.classList.add("hidden");
|
||||
};
|
||||
|
||||
const openMarketplaceModal = () => {
|
||||
closeLaunchModal();
|
||||
marketplaceModal?.classList.remove("hidden");
|
||||
if (marketplaceChannelSelect) {
|
||||
marketplaceChannelSelect.value = broadcaster?.toLowerCase() || marketplaceChannelSelect.value;
|
||||
}
|
||||
if (marketplaceSearchInput) {
|
||||
marketplaceSearchInput.value = "";
|
||||
}
|
||||
loadMarketplace();
|
||||
};
|
||||
|
||||
const closeMarketplaceModal = () => {
|
||||
marketplaceModal?.classList.add("hidden");
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
assetModal?.classList.remove("hidden");
|
||||
};
|
||||
@@ -34,9 +79,17 @@ export function createCustomAssetModal({ broadcaster, showToast = globalThis.sho
|
||||
};
|
||||
|
||||
const openNew = () => {
|
||||
closeLaunchModal();
|
||||
if (userNameInput) {
|
||||
userNameInput.value = "";
|
||||
}
|
||||
if (descriptionInput) {
|
||||
descriptionInput.value = "";
|
||||
}
|
||||
if (publicCheckbox) {
|
||||
publicCheckbox.checked = false;
|
||||
}
|
||||
resetLogoState();
|
||||
if (userSourceTextArea) {
|
||||
userSourceTextArea.value = "";
|
||||
userSourceTextArea.disabled = false;
|
||||
@@ -57,6 +110,19 @@ export function createCustomAssetModal({ broadcaster, showToast = globalThis.sho
|
||||
if (userNameInput) {
|
||||
userNameInput.value = asset.name || "";
|
||||
}
|
||||
if (descriptionInput) {
|
||||
descriptionInput.value = asset.description || "";
|
||||
}
|
||||
if (publicCheckbox) {
|
||||
publicCheckbox.checked = !!asset.isPublic;
|
||||
}
|
||||
resetLogoState();
|
||||
if (logoPreview && asset.logoUrl) {
|
||||
const img = document.createElement("img");
|
||||
img.src = asset.logoUrl;
|
||||
img.alt = asset.name || "Script logo";
|
||||
logoPreview.appendChild(img);
|
||||
}
|
||||
if (userSourceTextArea) {
|
||||
userSourceTextArea.value = "";
|
||||
userSourceTextArea.placeholder = "Loading script...";
|
||||
@@ -119,18 +185,28 @@ export function createCustomAssetModal({ broadcaster, showToast = globalThis.sho
|
||||
return false;
|
||||
}
|
||||
const assetId = userSourceTextArea?.dataset?.assetId;
|
||||
const description = descriptionInput?.value?.trim();
|
||||
const isPublic = !!publicCheckbox?.checked;
|
||||
const submitButton = formEvent.currentTarget?.querySelector('button[type="submit"]');
|
||||
if (submitButton) {
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = "Saving...";
|
||||
}
|
||||
saveCodeAsset({ name, src, assetId })
|
||||
saveCodeAsset({ name, src, assetId, description, isPublic })
|
||||
.then((asset) => {
|
||||
if (asset) {
|
||||
onAssetSaved?.(asset);
|
||||
return syncLogoChanges(asset).then((updated) => {
|
||||
onAssetSaved?.(updated || asset);
|
||||
return updated || asset;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.then((asset) => {
|
||||
closeModal();
|
||||
showToast?.(assetId ? "Custom asset updated." : "Custom asset created.", "success");
|
||||
if (asset) {
|
||||
showToast?.(assetId ? "Custom asset updated." : "Custom asset created.", "success");
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
showToast?.("Unable to save custom asset. Please try again.", "error");
|
||||
@@ -145,6 +221,29 @@ export function createCustomAssetModal({ broadcaster, showToast = globalThis.sho
|
||||
return false;
|
||||
};
|
||||
|
||||
if (launchModal) {
|
||||
launchModal.addEventListener("click", (event) => {
|
||||
if (event.target === launchModal) {
|
||||
closeLaunchModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (launchNewButton) {
|
||||
launchNewButton.addEventListener("click", () => openNew());
|
||||
}
|
||||
if (launchMarketplaceButton) {
|
||||
launchMarketplaceButton.addEventListener("click", () => openMarketplaceModal());
|
||||
}
|
||||
if (marketplaceModal) {
|
||||
marketplaceModal.addEventListener("click", (event) => {
|
||||
if (event.target === marketplaceModal) {
|
||||
closeMarketplaceModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (marketplaceCloseButton) {
|
||||
marketplaceCloseButton.addEventListener("click", () => closeMarketplaceModal());
|
||||
}
|
||||
if (assetModal) {
|
||||
assetModal.addEventListener("click", (event) => {
|
||||
if (event.target === assetModal) {
|
||||
@@ -158,6 +257,36 @@ export function createCustomAssetModal({ broadcaster, showToast = globalThis.sho
|
||||
if (cancelButton) {
|
||||
cancelButton.addEventListener("click", () => closeModal());
|
||||
}
|
||||
if (logoInput) {
|
||||
logoInput.addEventListener("change", (event) => {
|
||||
const file = event.target?.files?.[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
pendingLogoFile = file;
|
||||
logoRemoved = false;
|
||||
renderLogoPreview(file);
|
||||
});
|
||||
}
|
||||
if (logoClearButton) {
|
||||
logoClearButton.addEventListener("click", () => {
|
||||
logoRemoved = true;
|
||||
pendingLogoFile = null;
|
||||
if (logoInput) {
|
||||
logoInput.value = "";
|
||||
}
|
||||
clearLogoPreview();
|
||||
});
|
||||
}
|
||||
if (marketplaceSearchInput) {
|
||||
const handler = debounce((event) => {
|
||||
loadMarketplace(event.target?.value);
|
||||
}, 250);
|
||||
marketplaceSearchInput.addEventListener("input", handler);
|
||||
}
|
||||
if (marketplaceChannelSelect) {
|
||||
buildChannelOptions();
|
||||
}
|
||||
if (attachmentInput) {
|
||||
attachmentInput.addEventListener("change", (event) => {
|
||||
const file = event.target?.files?.[0];
|
||||
@@ -187,7 +316,7 @@ export function createCustomAssetModal({ broadcaster, showToast = globalThis.sho
|
||||
});
|
||||
}
|
||||
|
||||
return { openNew, openEditor };
|
||||
return { openLauncher: openLaunchModal, openNew, openEditor };
|
||||
|
||||
function setAttachmentState(assetId, attachments) {
|
||||
currentAssetId = assetId || null;
|
||||
@@ -300,8 +429,13 @@ export function createCustomAssetModal({ broadcaster, showToast = globalThis.sho
|
||||
});
|
||||
}
|
||||
|
||||
function saveCodeAsset({ name, src, assetId }) {
|
||||
const payload = { name, source: src };
|
||||
function saveCodeAsset({ name, src, assetId, description, isPublic }) {
|
||||
const payload = {
|
||||
name,
|
||||
source: src,
|
||||
description: description || null,
|
||||
isPublic,
|
||||
};
|
||||
const method = assetId ? "PUT" : "POST";
|
||||
const url = assetId
|
||||
? `/api/channels/${encodeURIComponent(broadcaster)}/assets/${assetId}/code`
|
||||
@@ -318,6 +452,195 @@ export function createCustomAssetModal({ broadcaster, showToast = globalThis.sho
|
||||
});
|
||||
}
|
||||
|
||||
function resetLogoState() {
|
||||
pendingLogoFile = null;
|
||||
logoRemoved = false;
|
||||
if (logoInput) {
|
||||
logoInput.value = "";
|
||||
}
|
||||
clearLogoPreview();
|
||||
}
|
||||
|
||||
function clearLogoPreview() {
|
||||
if (logoPreview) {
|
||||
logoPreview.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
function renderLogoPreview(file) {
|
||||
if (!logoPreview) {
|
||||
return;
|
||||
}
|
||||
clearLogoPreview();
|
||||
const img = document.createElement("img");
|
||||
img.alt = "Script logo preview";
|
||||
if (file instanceof File) {
|
||||
const url = URL.createObjectURL(file);
|
||||
img.src = url;
|
||||
img.onload = () => URL.revokeObjectURL(url);
|
||||
}
|
||||
logoPreview.appendChild(img);
|
||||
}
|
||||
|
||||
function syncLogoChanges(asset) {
|
||||
if (!asset?.id) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
if (logoRemoved) {
|
||||
return fetch(`/api/channels/${encodeURIComponent(broadcaster)}/assets/${asset.id}/logo`, {
|
||||
method: "DELETE",
|
||||
}).then(() => {
|
||||
logoRemoved = false;
|
||||
return { ...asset, logoUrl: null };
|
||||
});
|
||||
}
|
||||
if (!pendingLogoFile) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
const payload = new FormData();
|
||||
payload.append("file", pendingLogoFile);
|
||||
return fetch(`/api/channels/${encodeURIComponent(broadcaster)}/assets/${asset.id}/logo`, {
|
||||
method: "POST",
|
||||
body: payload,
|
||||
}).then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to upload logo");
|
||||
}
|
||||
pendingLogoFile = null;
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
|
||||
function buildChannelOptions() {
|
||||
if (!marketplaceChannelSelect) {
|
||||
return;
|
||||
}
|
||||
const channels = [broadcaster, ...adminChannels].filter(Boolean);
|
||||
const uniqueChannels = [...new Set(channels.map((channel) => channel.toLowerCase()))];
|
||||
marketplaceChannelSelect.innerHTML = "";
|
||||
uniqueChannels.forEach((channel) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = channel;
|
||||
option.textContent = channel;
|
||||
marketplaceChannelSelect.appendChild(option);
|
||||
});
|
||||
marketplaceChannelSelect.value = broadcaster?.toLowerCase() || uniqueChannels[0] || "";
|
||||
}
|
||||
|
||||
function loadMarketplace(query = "") {
|
||||
if (!marketplaceList) {
|
||||
return;
|
||||
}
|
||||
const queryString = query ? `?query=${encodeURIComponent(query)}` : "";
|
||||
marketplaceList.innerHTML = '<div class="marketplace-loading">Loading scripts...</div>';
|
||||
fetch(`/api/marketplace/scripts${queryString}`)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load marketplace");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((entries) => {
|
||||
marketplaceEntries = Array.isArray(entries) ? entries : [];
|
||||
renderMarketplace();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
marketplaceList.innerHTML =
|
||||
'<div class="marketplace-empty">Unable to load marketplace scripts.</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderMarketplace() {
|
||||
if (!marketplaceList) {
|
||||
return;
|
||||
}
|
||||
marketplaceList.innerHTML = "";
|
||||
if (!marketplaceEntries.length) {
|
||||
marketplaceList.innerHTML = '<div class="marketplace-empty">No scripts found.</div>';
|
||||
return;
|
||||
}
|
||||
marketplaceEntries.forEach((entry) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "marketplace-card";
|
||||
|
||||
if (entry.logoUrl) {
|
||||
const logo = document.createElement("img");
|
||||
logo.src = entry.logoUrl;
|
||||
logo.alt = entry.name || "Script logo";
|
||||
logo.className = "marketplace-logo";
|
||||
card.appendChild(logo);
|
||||
} else {
|
||||
const placeholder = document.createElement("div");
|
||||
placeholder.className = "marketplace-logo placeholder";
|
||||
placeholder.innerHTML = '<i class="fa-solid fa-code"></i>';
|
||||
card.appendChild(placeholder);
|
||||
}
|
||||
|
||||
const content = document.createElement("div");
|
||||
content.className = "marketplace-content";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = entry.name || "Untitled script";
|
||||
const description = document.createElement("p");
|
||||
description.textContent = entry.description || "No description provided.";
|
||||
const meta = document.createElement("small");
|
||||
meta.textContent = entry.broadcaster ? `By ${entry.broadcaster}` : "";
|
||||
content.appendChild(title);
|
||||
content.appendChild(description);
|
||||
content.appendChild(meta);
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "marketplace-actions";
|
||||
const importButton = document.createElement("button");
|
||||
importButton.type = "button";
|
||||
importButton.className = "primary";
|
||||
importButton.textContent = "Import";
|
||||
importButton.addEventListener("click", () => importMarketplaceScript(entry));
|
||||
actions.appendChild(importButton);
|
||||
|
||||
card.appendChild(content);
|
||||
card.appendChild(actions);
|
||||
marketplaceList.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function importMarketplaceScript(entry) {
|
||||
if (!entry?.id) {
|
||||
return;
|
||||
}
|
||||
const target = marketplaceChannelSelect?.value || broadcaster;
|
||||
fetch(`/api/marketplace/scripts/${entry.id}/import`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ targetBroadcaster: target }),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to import script");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((asset) => {
|
||||
closeMarketplaceModal();
|
||||
showToast?.("Script imported.", "success");
|
||||
onAssetSaved?.(asset);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
showToast?.("Unable to import script. Please try again.", "error");
|
||||
});
|
||||
}
|
||||
|
||||
function debounce(fn, wait = 150) {
|
||||
let timeout;
|
||||
return (...args) => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => fn(...args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
function getUserJavaScriptSourceError(src) {
|
||||
let ast;
|
||||
|
||||
|
||||
@@ -368,6 +368,47 @@
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div id="custom-asset-launch-modal" class="modal hidden">
|
||||
<section class="modal-inner small">
|
||||
<h1>Custom scripts</h1>
|
||||
<p>Start a new script or browse scripts shared by other creators.</p>
|
||||
<div class="form-actions split">
|
||||
<button type="button" class="secondary" id="custom-asset-launch-marketplace">
|
||||
Browse marketplace
|
||||
</button>
|
||||
<button type="button" class="primary" id="custom-asset-launch-new">
|
||||
Create new script
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div id="custom-asset-marketplace-modal" class="modal hidden">
|
||||
<section class="modal-inner wide">
|
||||
<div class="modal-header-row">
|
||||
<div>
|
||||
<h1>Custom script marketplace</h1>
|
||||
<p>Search public scripts by name or description.</p>
|
||||
</div>
|
||||
<button type="button" class="ghost icon-button" id="custom-asset-marketplace-close" aria-label="Close">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="custom-asset-marketplace-search">Search scripts</label>
|
||||
<input
|
||||
id="custom-asset-marketplace-search"
|
||||
type="search"
|
||||
class="text-input"
|
||||
placeholder="Search by name or description"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="custom-asset-marketplace-channel">Import into channel</label>
|
||||
<select id="custom-asset-marketplace-channel" class="text-input"></select>
|
||||
</div>
|
||||
<div class="marketplace-list" id="custom-asset-marketplace-list"></div>
|
||||
</section>
|
||||
</div>
|
||||
<div id="custom-asset-modal" class="modal hidden">
|
||||
<section class="modal-inner">
|
||||
<h1>Create Custom Asset</h1>
|
||||
@@ -382,6 +423,39 @@
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="custom-asset-description">Description</label>
|
||||
<textarea
|
||||
id="custom-asset-description"
|
||||
class="text-input"
|
||||
placeholder="Describe what this script does"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Logo (optional)</label>
|
||||
<div class="attachment-actions">
|
||||
<input
|
||||
id="custom-asset-logo-file"
|
||||
class="file-input-field"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
/>
|
||||
<label for="custom-asset-logo-file" class="file-input-trigger small">
|
||||
<span class="file-input-icon"><i class="fa-solid fa-image"></i></span>
|
||||
<span class="file-input-copy">
|
||||
<strong>Upload logo</strong>
|
||||
<small>PNG, JPG, or GIF</small>
|
||||
</span>
|
||||
</label>
|
||||
<button type="button" class="secondary" id="custom-asset-logo-clear">Remove logo</button>
|
||||
</div>
|
||||
<div class="logo-preview" id="custom-asset-logo-preview"></div>
|
||||
</div>
|
||||
<div class="form-group checkbox-row">
|
||||
<input id="custom-asset-public" type="checkbox" />
|
||||
<label for="custom-asset-public">Make this script public in the marketplace</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="custom-asset-type">Asset code</label>
|
||||
<textarea
|
||||
@@ -434,6 +508,7 @@
|
||||
const username = /*[[${username}]]*/ "";
|
||||
const UPLOAD_LIMIT_BYTES = /*[[${uploadLimitBytes}]]*/ 0;
|
||||
const SETTINGS = JSON.parse(/*[[${settingsJson}]]*/);
|
||||
const ADMIN_CHANNELS = /*[[${adminChannels}]]*/ [];
|
||||
</script>
|
||||
<script src="/js/cookie-consent.js"></script>
|
||||
<script src="/js/toast.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user