mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
539 lines
31 KiB
HTML
539 lines
31 KiB
HTML
<!doctype html>
|
|
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>Imgfloat Admin</title>
|
|
<meta name="_csrf" th:content="${_csrf.token}" />
|
|
<meta name="_csrf_header" th:content="${_csrf.headerName}" />
|
|
<link rel="icon" href="/favicon.ico" />
|
|
<link rel="stylesheet" href="/css/styles.css" />
|
|
<link rel="stylesheet" href="/css/customAssets.css" />
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.css" />
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/lint/lint.css" />
|
|
<link
|
|
rel="stylesheet"
|
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
|
integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A=="
|
|
crossorigin="anonymous"
|
|
referrerpolicy="no-referrer"
|
|
/>
|
|
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/acorn@8.15.0/dist/acorn.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/javascript/javascript.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/lint/lint.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/display/placeholder.min.js"></script>
|
|
<script src="/js/vendor/three.min.js"></script>
|
|
<script src="/js/vendor/GLTFLoader.js"></script>
|
|
<script src="/js/vendor/OBJLoader.js"></script>
|
|
<script src="/js/csrf.js"></script>
|
|
</head>
|
|
<body class="admin-body" th:classappend="${isStaging} ? ' has-staging-banner' : ''">
|
|
<div th:insert="~{fragments/staging :: banner}"></div>
|
|
<div class="admin-frame">
|
|
<header class="admin-topbar">
|
|
<div class="topbar-left">
|
|
<div class="admin-identity">
|
|
<p class="eyebrow subtle">CHANNEL ADMIN</p>
|
|
<h1 th:text="${broadcaster}"></h1>
|
|
</div>
|
|
</div>
|
|
<div class="header-actions horizontal">
|
|
<a class="icon-button" th:href="@{/}" title="Back to dashboard">
|
|
<i class="fa-solid fa-chevron-left" aria-hidden="true"></i>
|
|
<span class="sr-only">Back to dashboard</span>
|
|
</a>
|
|
<a
|
|
class="button ghost"
|
|
th:if="${#strings.equalsIgnoreCase(username, broadcaster)}"
|
|
th:href="${'/view/' + broadcaster + '/audit'}"
|
|
>Audit log</a
|
|
>
|
|
<a
|
|
class="button ghost"
|
|
th:href="${'/view/' + broadcaster + '/broadcast'}"
|
|
target="_blank"
|
|
rel="noopener"
|
|
>Broadcaster view</a
|
|
>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="admin-workspace">
|
|
<aside class="admin-rail">
|
|
<div class="upload-row">
|
|
<button type="button" class="file-input-trigger" id="asset-launcher-button">
|
|
<span class="file-input-icon"><i class="fa-solid fa-layer-group"></i></span>
|
|
<span class="file-input-copy">
|
|
<strong>Add asset</strong>
|
|
<small>Upload, build, or browse scripts</small>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<div class="rail-body">
|
|
<div class="rail-scroll">
|
|
<ul id="asset-list" class="asset-list"></ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="asset-inspector" class="rail-inspector hidden">
|
|
<div class="asset-inspector">
|
|
<div class="selected-asset-banner">
|
|
<div class="selected-asset-main">
|
|
<div class="title-row">
|
|
<strong id="selected-asset-name">Choose an asset</strong>
|
|
<span
|
|
id="selected-asset-resolution"
|
|
class="asset-resolution subtle-text hidden"
|
|
></span>
|
|
</div>
|
|
<p class="meta-text" id="selected-asset-meta">
|
|
Pick an asset in the list to adjust its placement and playback.
|
|
</p>
|
|
<p class="meta-text subtle-text hidden" id="selected-asset-id"></p>
|
|
<div class="badge-row asset-meta-badges" id="selected-asset-badges"></div>
|
|
</div>
|
|
</div>
|
|
<div id="asset-controls-placeholder" class="asset-controls-placeholder">
|
|
<div id="asset-controls" class="hidden asset-settings">
|
|
<div class="panel-section" id="layout-section">
|
|
<div class="section-header">
|
|
<h5>Layout & order</h5>
|
|
</div>
|
|
<div class="property-list">
|
|
<div class="property-row">
|
|
<span class="property-label">Width</span>
|
|
<input
|
|
id="asset-width"
|
|
class="number-input property-control"
|
|
type="number"
|
|
min="10"
|
|
step="5"
|
|
/>
|
|
</div>
|
|
<div class="property-row">
|
|
<span class="property-label">Height</span>
|
|
<input
|
|
id="asset-height"
|
|
class="number-input property-control"
|
|
type="number"
|
|
min="10"
|
|
step="5"
|
|
/>
|
|
</div>
|
|
<div class="property-row">
|
|
<span class="property-label">Maintain AR</span>
|
|
<label class="checkbox-inline toggle inline-toggle property-control">
|
|
<input id="maintain-aspect" type="checkbox" checked />
|
|
<span class="toggle-track" aria-hidden="true">
|
|
<span class="toggle-thumb"></span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
<div class="property-row">
|
|
<span class="property-label">Layer</span>
|
|
<div class="property-control">
|
|
<div class="badge-row stacked">
|
|
<span class="badge"
|
|
>Layer <strong id="asset-z-level">1</strong></span
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel-section" id="playback-section">
|
|
<div class="section-header">
|
|
<h5>Playback</h5>
|
|
</div>
|
|
<div class="stacked-field">
|
|
<div class="label-row">
|
|
<span>Playback speed</span>
|
|
<span class="value-hint" id="asset-speed-label">100%</span>
|
|
</div>
|
|
<input
|
|
id="asset-speed"
|
|
class="range-input"
|
|
type="range"
|
|
min="0"
|
|
max="1000"
|
|
step="10"
|
|
value="100"
|
|
/>
|
|
<div class="range-meta"><span>0%</span><span>1000%</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel-section" id="volume-section">
|
|
<div class="section-header">
|
|
<h5>Volume</h5>
|
|
</div>
|
|
<div class="stacked-field">
|
|
<div class="label-row">
|
|
<span>Playback volume</span>
|
|
<span class="value-hint" id="asset-volume-label">100%</span>
|
|
</div>
|
|
<input
|
|
id="asset-volume"
|
|
class="range-input"
|
|
type="range"
|
|
min="0"
|
|
max="200"
|
|
step="1"
|
|
value="100"
|
|
/>
|
|
<div class="range-meta"><span>0%</span><span>200%</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel-section hidden" id="audio-section">
|
|
<div class="section-header">
|
|
<h5>Audio</h5>
|
|
</div>
|
|
<div class="property-list">
|
|
<div class="property-row">
|
|
<span class="property-label">Loop</span>
|
|
<label class="checkbox-inline toggle inline-toggle property-control">
|
|
<input id="asset-audio-loop" type="checkbox" />
|
|
<span class="toggle-track" aria-hidden="true">
|
|
<span class="toggle-thumb"></span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="stacked-field">
|
|
<div class="label-row">
|
|
<span>Delay</span>
|
|
<span class="value-hint" id="asset-audio-delay-label">0ms</span>
|
|
</div>
|
|
<input
|
|
id="asset-audio-delay"
|
|
class="range-input property-control"
|
|
type="range"
|
|
min="0"
|
|
max="30000"
|
|
step="100"
|
|
value="0"
|
|
/>
|
|
<div class="range-meta"><span>0ms</span><span>30s</span></div>
|
|
</div>
|
|
<div class="stacked-field">
|
|
<div class="label-row">
|
|
<span>Playback speed</span>
|
|
<span class="value-hint" id="asset-audio-speed-label">1.0x</span>
|
|
</div>
|
|
<input
|
|
id="asset-audio-speed"
|
|
class="range-input"
|
|
type="range"
|
|
min="25"
|
|
max="400"
|
|
step="5"
|
|
value="100"
|
|
/>
|
|
<div class="range-meta"><span>0.25x</span><span>4x</span></div>
|
|
</div>
|
|
<div class="stacked-field">
|
|
<div class="label-row">
|
|
<span>Pitch</span>
|
|
<span class="value-hint" id="asset-audio-pitch-label">100%</span>
|
|
</div>
|
|
<input
|
|
id="asset-audio-pitch"
|
|
class="range-input property-control"
|
|
type="range"
|
|
min="50"
|
|
max="200"
|
|
step="5"
|
|
value="100"
|
|
/>
|
|
<div class="range-meta"><span>50%</span><span>200%</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="control-actions compact unified-actions" id="asset-actions">
|
|
<button
|
|
type="button"
|
|
onclick="sendToBack()"
|
|
class="secondary"
|
|
title="Send to back"
|
|
data-code-enabled="true"
|
|
>
|
|
<i class="fa-solid fa-angles-down"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onclick="bringBackward()"
|
|
class="secondary"
|
|
title="Move backward"
|
|
data-code-enabled="true"
|
|
>
|
|
<i class="fa-solid fa-arrow-down"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onclick="bringForward()"
|
|
class="secondary"
|
|
title="Move forward"
|
|
data-code-enabled="true"
|
|
>
|
|
<i class="fa-solid fa-arrow-up"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onclick="bringToFront()"
|
|
class="secondary"
|
|
title="Bring to front"
|
|
data-code-enabled="true"
|
|
>
|
|
<i class="fa-solid fa-angles-up"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onclick="recenterSelectedAsset()"
|
|
class="secondary"
|
|
title="Center on canvas"
|
|
>
|
|
<i class="fa-solid fa-bullseye"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onclick="nudgeRotation(-5)"
|
|
class="secondary"
|
|
title="Rotate left"
|
|
>
|
|
<i class="fa-solid fa-rotate-left"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onclick="nudgeRotation(5)"
|
|
class="secondary"
|
|
title="Rotate right"
|
|
>
|
|
<i class="fa-solid fa-rotate-right"></i>
|
|
</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
|
|
id="selected-asset-visibility"
|
|
class="secondary"
|
|
type="button"
|
|
title="Hide asset"
|
|
disabled
|
|
data-audio-enabled="true"
|
|
>
|
|
<i class="fa-solid fa-eye-slash"></i>
|
|
</button>
|
|
<button
|
|
id="selected-asset-delete"
|
|
class="secondary danger"
|
|
type="button"
|
|
title="Delete asset"
|
|
disabled
|
|
data-audio-enabled="true"
|
|
data-code-enabled="true"
|
|
>
|
|
<i class="fa-solid fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<section class="canvas-stack">
|
|
<div class="canvas-topbar">
|
|
<div>
|
|
<p class="eyebrow subtle">Canvas</p>
|
|
<h3 class="panel-title">Live composition</h3>
|
|
</div>
|
|
<div class="canvas-meta">
|
|
<span class="badge soft" id="canvas-resolution">1920 x 1080</span>
|
|
<span class="badge outline" id="canvas-scale">100%</span>
|
|
</div>
|
|
</div>
|
|
<div class="canvas-surface">
|
|
<div class="overlay canvas-boundary" id="admin-overlay">
|
|
<div class="canvas-guides"></div>
|
|
<canvas id="admin-canvas"></canvas>
|
|
</div>
|
|
<div class="canvas-footnote">
|
|
<p>Edges of the canvas are outlined to match the aspect ratio of the stream.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
<div id="custom-asset-launch-modal" class="modal hidden">
|
|
<section class="modal-inner medium">
|
|
<h1>Custom assets</h1>
|
|
<p>Upload media, build new scripts, or pull from the marketplace.</p>
|
|
<div class="launch-grid">
|
|
<input
|
|
id="asset-file"
|
|
class="file-input-field"
|
|
type="file"
|
|
accept="image/*,video/*,audio/*,model/*,.glb,.gltf,.obj,application/javascript,text/javascript,.js,.mjs"
|
|
/>
|
|
<label for="asset-file" class="launch-tile">
|
|
<span class="tile-icon"><i class="fa-solid fa-cloud-arrow-up"></i></span>
|
|
<span class="tile-title">Upload asset</span>
|
|
<span class="tile-subtitle" id="asset-file-name">No file chosen</span>
|
|
</label>
|
|
<button type="button" class="launch-tile" id="custom-asset-launch-new">
|
|
<span class="tile-icon"><i class="fa-solid fa-code"></i></span>
|
|
<span class="tile-title">Create script</span>
|
|
<span class="tile-subtitle">Start from a blank template</span>
|
|
</button>
|
|
<button type="button" class="launch-tile" id="custom-asset-launch-marketplace">
|
|
<span class="tile-icon"><i class="fa-solid fa-store"></i></span>
|
|
<span class="tile-title">Browse marketplace</span>
|
|
<span class="tile-subtitle">Find community scripts</span>
|
|
</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>
|
|
<form id="custom-asset-form">
|
|
<div class="form-group">
|
|
<label for="custom-asset-name">Asset name</label>
|
|
<input
|
|
id="custom-asset-name"
|
|
type="text"
|
|
class="text-input"
|
|
placeholder="Enter asset name"
|
|
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
|
|
class="text-input"
|
|
id="custom-asset-code"
|
|
placeholder="exports.init = (context) => { const { assets } = context; }; exports.tick = () => { };"
|
|
rows="25"
|
|
required
|
|
></textarea>
|
|
<p class="field-note">
|
|
By submitting your script, you agree to release it under the MIT License to the public.
|
|
</p>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Script attachments</label>
|
|
<div class="attachment-actions">
|
|
<input
|
|
id="custom-asset-attachment-file"
|
|
class="file-input-field"
|
|
type="file"
|
|
accept="image/*,video/*,audio/*,model/*,.glb,.gltf,.obj"
|
|
/>
|
|
<label for="custom-asset-attachment-file" class="file-input-trigger small">
|
|
<span class="file-input-icon"><i class="fa-solid fa-paperclip"></i></span>
|
|
<span class="file-input-copy">
|
|
<strong>Add attachment</strong>
|
|
<small>Images, video, audio, or 3D models</small>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
<p class="field-note" id="custom-asset-attachment-hint">
|
|
Attachments are stored with the script and are available for scripts to render. Save the
|
|
script before adding attachments.
|
|
</p>
|
|
<ul id="custom-asset-attachment-list" class="attachment-list"></ul>
|
|
</div>
|
|
<div class="form-error hidden" id="custom-asset-error">
|
|
<strong>JavaScript error: <span id="js-error-title"></span></strong>
|
|
<pre id="js-error-details"></pre>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="secondary" id="custom-asset-cancel">Cancel</button>
|
|
<button type="submit" class="primary">Test and save</button>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
</div>
|
|
<script th:inline="javascript">
|
|
const broadcaster = /*[[${broadcaster}]]*/ "";
|
|
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>
|
|
<script type="module" src="/js/admin.js"></script>
|
|
</body>
|
|
</html>
|