Major refactor of admin view

This commit is contained in:
2026-01-09 00:36:11 +01:00
parent e8e2f9be15
commit d835dedb66
4 changed files with 2673 additions and 2631 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,34 @@
const assetModal = document.getElementById("custom-asset-modal"); export function createCustomAssetModal({ broadcaster, showToast = globalThis.showToast, onAssetSaved }) {
const userNameInput = document.getElementById("custom-asset-name"); const assetModal = document.getElementById("custom-asset-modal");
const userSourceTextArea = document.getElementById("custom-asset-code"); const userNameInput = document.getElementById("custom-asset-name");
const formErrorWrapper = document.getElementById("custom-asset-error"); const userSourceTextArea = document.getElementById("custom-asset-code");
const jsErrorTitle = document.getElementById("js-error-title"); const formErrorWrapper = document.getElementById("custom-asset-error");
const jsErrorDetails = document.getElementById("js-error-details"); const jsErrorTitle = document.getElementById("js-error-title");
const jsErrorDetails = document.getElementById("js-error-details");
const form = document.getElementById("custom-asset-form");
const cancelButton = document.getElementById("custom-asset-cancel");
function toggleCustomAssetModal(event) { const resetErrors = () => {
if (event !== undefined && event.target !== event.currentTarget) { if (formErrorWrapper) {
return; formErrorWrapper.classList.add("hidden");
} }
if (assetModal.classList.contains("hidden")) { if (jsErrorTitle) {
assetModal.classList.remove("hidden"); jsErrorTitle.textContent = "";
}
if (jsErrorDetails) {
jsErrorDetails.textContent = "";
}
};
const openModal = () => {
assetModal?.classList.remove("hidden");
};
const closeModal = () => {
assetModal?.classList.add("hidden");
};
const openNew = () => {
if (userNameInput) { if (userNameInput) {
userNameInput.value = ""; userNameInput.value = "";
} }
@@ -21,152 +39,194 @@ function toggleCustomAssetModal(event) {
userSourceTextArea.placeholder = userSourceTextArea.placeholder =
"function init({ surface, assets, channel }) {\n\n}\n\nfunction tick() {\n\n}"; "function init({ surface, assets, channel }) {\n\n}\n\nfunction tick() {\n\n}";
} }
if (formErrorWrapper) { resetErrors();
formErrorWrapper.classList.add("hidden"); openModal();
} };
if (jsErrorTitle) {
jsErrorTitle.textContent = "";
}
if (jsErrorDetails) {
jsErrorDetails.textContent = "";
}
} else {
assetModal.classList.add("hidden");
}
}
function submitCodeAsset(formEvent) { const openEditor = (asset) => {
formEvent.preventDefault(); if (!asset) {
const src = userSourceTextArea.value; return;
const error = getUserJavaScriptSourceError(src); }
if (error) { resetErrors();
jsErrorTitle.textContent = error.title; if (userNameInput) {
jsErrorDetails.textContent = error.details; userNameInput.value = asset.name || "";
formErrorWrapper.classList.remove("hidden"); }
if (userSourceTextArea) {
userSourceTextArea.value = "";
userSourceTextArea.placeholder = "Loading script...";
userSourceTextArea.disabled = true;
userSourceTextArea.dataset.assetId = asset.id;
}
openModal();
fetch(asset.url)
.then((response) => {
if (!response.ok) {
throw new Error("Failed to load script");
}
return response.text();
})
.then((text) => {
if (userSourceTextArea) {
userSourceTextArea.disabled = false;
userSourceTextArea.value = text;
}
})
.catch(() => {
if (userSourceTextArea) {
userSourceTextArea.disabled = false;
userSourceTextArea.value = "";
}
showToast?.("Unable to load script content.", "error");
});
};
const handleFormSubmit = (formEvent) => {
formEvent.preventDefault();
const src = userSourceTextArea?.value;
const error = getUserJavaScriptSourceError(src);
if (error) {
if (jsErrorTitle) {
jsErrorTitle.textContent = error.title;
}
if (jsErrorDetails) {
jsErrorDetails.textContent = error.details;
}
if (formErrorWrapper) {
formErrorWrapper.classList.remove("hidden");
}
return false;
}
resetErrors();
const name = userNameInput?.value?.trim();
if (!name) {
if (jsErrorTitle) {
jsErrorTitle.textContent = "Missing name";
}
if (jsErrorDetails) {
jsErrorDetails.textContent = "Please provide a name for your custom asset.";
}
if (formErrorWrapper) {
formErrorWrapper.classList.remove("hidden");
}
return false;
}
const assetId = userSourceTextArea?.dataset?.assetId;
const submitButton = formEvent.currentTarget?.querySelector('button[type="submit"]');
if (submitButton) {
submitButton.disabled = true;
submitButton.textContent = "Saving...";
}
saveCodeAsset({ name, src, assetId })
.then((asset) => {
if (asset) {
onAssetSaved?.(asset);
}
closeModal();
showToast?.(assetId ? "Custom asset updated." : "Custom asset created.", "success");
})
.catch((e) => {
showToast?.("Unable to save custom asset. Please try again.", "error");
console.error(e);
})
.finally(() => {
if (submitButton) {
submitButton.disabled = false;
submitButton.textContent = "Test and save";
}
});
return false; return false;
} };
formErrorWrapper.classList.add("hidden");
jsErrorTitle.textContent = ""; if (assetModal) {
jsErrorDetails.textContent = ""; assetModal.addEventListener("click", (event) => {
const name = userNameInput?.value?.trim(); if (event.target === assetModal) {
if (!name) { closeModal();
jsErrorTitle.textContent = "Missing name";
jsErrorDetails.textContent = "Please provide a name for your custom asset.";
formErrorWrapper.classList.remove("hidden");
return false;
}
const assetId = userSourceTextArea?.dataset?.assetId;
const submitButton = formEvent.currentTarget?.querySelector('button[type="submit"]');
if (submitButton) {
submitButton.disabled = true;
submitButton.textContent = "Saving...";
}
saveCodeAsset({ name, src, assetId })
.then((asset) => {
if (asset && typeof storeAsset === "function") {
storeAsset(asset);
if (typeof updateRenderState === "function") {
updateRenderState(asset);
}
if (typeof selectedAssetId !== "undefined") {
selectedAssetId = asset.id;
}
if (typeof updateSelectedAssetControls === "function") {
updateSelectedAssetControls(asset);
}
if (typeof drawAndList === "function") {
drawAndList();
}
}
if (assetModal) {
assetModal.classList.add("hidden");
}
if (typeof showToast === "function") {
showToast(assetId ? "Custom asset updated." : "Custom asset created.", "success");
}
})
.catch((e) => {
if (typeof showToast === "function") {
showToast("Unable to save custom asset. Please try again.", "error");
}
console.error(e);
})
.finally(() => {
if (submitButton) {
submitButton.disabled = false;
submitButton.textContent = "Test and save";
} }
}); });
return false; }
} if (form) {
form.addEventListener("submit", handleFormSubmit);
}
if (cancelButton) {
cancelButton.addEventListener("click", () => closeModal());
}
function saveCodeAsset({ name, src, assetId }) { return { openNew, openEditor };
const payload = { name, source: src };
const method = assetId ? "PUT" : "POST";
const url = assetId
? `/api/channels/${encodeURIComponent(broadcaster)}/assets/${assetId}/code`
: `/api/channels/${encodeURIComponent(broadcaster)}/assets/code`;
return fetch(url, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}).then((response) => {
if (!response.ok) {
throw new Error("Failed to save code asset");
}
return response.json();
});
}
function getUserJavaScriptSourceError(src) { function saveCodeAsset({ name, src, assetId }) {
let ast; const payload = { name, source: src };
const method = assetId ? "PUT" : "POST";
try { const url = assetId
ast = acorn.parse(src, { ? `/api/channels/${encodeURIComponent(broadcaster)}/assets/${assetId}/code`
ecmaVersion: "latest", : `/api/channels/${encodeURIComponent(broadcaster)}/assets/code`;
sourceType: "script", return fetch(url, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}).then((response) => {
if (!response.ok) {
throw new Error("Failed to save code asset");
}
return response.json();
}); });
} catch (e) {
return { title: "Syntax Error", details: e.message };
} }
let hasInit = false; function getUserJavaScriptSourceError(src) {
let hasTick = false; let ast;
for (const node of ast.body) { const parser = globalThis.acorn;
if (node.type !== "ExpressionStatement") continue; if (!parser) {
return { title: "Parser unavailable", details: "JavaScript parser is not available yet." };
const expr = node.expression;
if (expr.type !== "AssignmentExpression") continue;
const left = expr.left;
const right = expr.right;
if (
left.type === "MemberExpression" &&
left.object.type === "Identifier" &&
left.object.name === "exports" &&
left.property.type === "Identifier" &&
(right.type === "FunctionExpression" || right.type === "ArrowFunctionExpression")
) {
if (left.property.name === "init") hasInit = true;
if (left.property.name === "tick") hasTick = true;
} }
}
if (!hasInit) { try {
return { ast = parser.parse(src, {
title: "Missing function: init", ecmaVersion: "latest",
details: "You must assign a function to exports.init", sourceType: "script",
}; });
} } catch (e) {
return { title: "Syntax Error", details: e.message };
}
if (!hasTick) { let hasInit = false;
return { let hasTick = false;
title: "Missing function: tick",
details: "You must assign a function to exports.tick",
};
}
return undefined; for (const node of ast.body) {
if (node.type !== "ExpressionStatement") continue;
const expr = node.expression;
if (expr.type !== "AssignmentExpression") continue;
const left = expr.left;
const right = expr.right;
if (
left.type === "MemberExpression" &&
left.object.type === "Identifier" &&
left.object.name === "exports" &&
left.property.type === "Identifier" &&
(right.type === "FunctionExpression" || right.type === "ArrowFunctionExpression")
) {
if (left.property.name === "init") hasInit = true;
if (left.property.name === "tick") hasTick = true;
}
}
if (!hasInit) {
return {
title: "Missing function: init",
details: "You must assign a function to exports.init",
};
}
if (!hasTick) {
return {
title: "Missing function: tick",
details: "You must assign a function to exports.tick",
};
}
return undefined;
}
} }

View File

@@ -52,7 +52,6 @@
class="file-input-field" class="file-input-field"
type="file" type="file"
accept="image/*,video/*,audio/*,application/javascript,text/javascript,.js,.mjs" accept="image/*,video/*,audio/*,application/javascript,text/javascript,.js,.mjs"
onchange="handleFileSelection(this)"
/> />
<label for="asset-file" class="file-input-trigger"> <label for="asset-file" class="file-input-trigger">
<span class="file-input-icon"><i class="fa-solid fa-cloud-arrow-up"></i></span> <span class="file-input-icon"><i class="fa-solid fa-cloud-arrow-up"></i></span>
@@ -373,10 +372,10 @@
</section> </section>
</div> </div>
</div> </div>
<div id="custom-asset-modal" class="modal hidden" onclick="toggleCustomAssetModal(event)"> <div id="custom-asset-modal" class="modal hidden">
<section class="modal-inner"> <section class="modal-inner">
<h1>Create Custom Asset</h1> <h1>Create Custom Asset</h1>
<form id="custom-asset-form" onsubmit="submitCodeAsset(event)"> <form id="custom-asset-form">
<div class="form-group"> <div class="form-group">
<label for="custom-asset-name">Asset name</label> <label for="custom-asset-name">Asset name</label>
<input <input
@@ -402,7 +401,7 @@
<pre id="js-error-details"></pre> <pre id="js-error-details"></pre>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="secondary" onclick="toggleCustomAssetModal(event)">Cancel</button> <button type="button" class="secondary" id="custom-asset-cancel">Cancel</button>
<button type="submit" class="primary">Test and save</button> <button type="submit" class="primary">Test and save</button>
</div> </div>
</form> </form>
@@ -416,7 +415,6 @@
</script> </script>
<script src="/js/cookie-consent.js"></script> <script src="/js/cookie-consent.js"></script>
<script src="/js/toast.js"></script> <script src="/js/toast.js"></script>
<script src="/js/customAssets.js"></script>
<script type="module" src="/js/admin.js"></script> <script type="module" src="/js/admin.js"></script>
</body> </body>
</html> </html>