mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Major refactor of admin view
This commit is contained in:
File diff suppressed because it is too large
Load Diff
2449
src/main/resources/static/js/admin/console.js
Normal file
2449
src/main/resources/static/js/admin/console.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user