Add prettier

This commit is contained in:
2026-01-05 11:34:53 +01:00
parent c0ca04a349
commit 65488ae59e
18 changed files with 4981 additions and 4662 deletions

View File

@@ -1,216 +1,306 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<head>
<meta charset="UTF-8" />
<title>Imgfloat Admin</title>
<link rel="stylesheet" href="/css/styles.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" />
<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>
</head>
<body class="admin-body">
<div class="admin-frame">
<header class="admin-topbar">
</head>
<body class="admin-body">
<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 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:href="${'/view/' + broadcaster + '/broadcast'}" target="_blank" rel="noopener">Broadcaster view</a>
<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:href="${'/view/' + broadcaster + '/broadcast'}" target="_blank" rel="noopener"
>Broadcaster view</a
>
</div>
</header>
</header>
<div class="admin-workspace">
<div class="admin-workspace">
<aside class="admin-rail">
<div class="upload-row">
<input id="asset-file" class="file-input-field" type="file" accept="image/*,video/*,audio/*" onchange="handleFileSelection(this)" />
<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-copy">
<strong>Upload asset</strong>
<small id="asset-file-name">No file chosen</small>
</span>
</label>
<div class="upload-row">
<input
id="asset-file"
class="file-input-field"
type="file"
accept="image/*,video/*,audio/*"
onchange="handleFileSelection(this)"
/>
<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-copy">
<strong>Upload asset</strong>
<small id="asset-file-name">No file chosen</small>
</span>
</label>
</div>
<div class="rail-body">
<div class="rail-scroll">
<ul id="asset-list" class="asset-list"></ul>
</div>
<div class="rail-body">
<div class="rail-scroll">
<ul id="asset-list" class="asset-list"></ul>
</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-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 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"><i class="fa-solid fa-angles-down"></i></button>
<button type="button" onclick="bringBackward()" class="secondary" title="Move backward"><i class="fa-solid fa-arrow-down"></i></button>
<button type="button" onclick="bringForward()" class="secondary" title="Move forward"><i class="fa-solid fa-arrow-up"></i></button>
<button type="button" onclick="bringToFront()" class="secondary" title="Bring to front"><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-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">
<i class="fa-solid fa-trash"></i>
</button>
</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">
<i class="fa-solid fa-angles-down"></i>
</button>
<button type="button" onclick="bringBackward()" class="secondary" title="Move backward">
<i class="fa-solid fa-arrow-down"></i>
</button>
<button type="button" onclick="bringForward()" class="secondary" title="Move forward">
<i class="fa-solid fa-arrow-up"></i>
</button>
<button type="button" onclick="bringToFront()" class="secondary" title="Bring to front">
<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-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"
>
<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 class="canvas-topbar">
<div>
<p class="eyebrow subtle">Canvas</p>
<h3 class="panel-title">Live composition</h3>
</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 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>
<script th:inline="javascript">
const broadcaster = /*[[${broadcaster}]]*/ '';
const username = /*[[${username}]]*/ '';
const UPLOAD_LIMIT_BYTES = /*[[${uploadLimitBytes}]]*/ 0;
const SETTINGS = /*[[${settingsJson}]]*/;
</script>
<script src="/js/toast.js"></script>
<script src="/js/admin.js"></script>
</body>
<script th:inline="javascript">
const broadcaster = /*[[${broadcaster}]]*/ '';
const username = /*[[${username}]]*/ '';
const UPLOAD_LIMIT_BYTES = /*[[${uploadLimitBytes}]]*/ 0;
const SETTINGS = /*[[${settingsJson}]]*/;
</script>
<script src="/js/toast.js"></script>
<script src="/js/admin.js"></script>
</body>
</html>

View File

@@ -1,18 +1,18 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<head>
<meta charset="UTF-8" />
<title>Imgfloat Broadcast</title>
<link rel="stylesheet" href="/css/styles.css" />
<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>
</head>
<body class="broadcast-body">
<canvas id="broadcast-canvas"></canvas>
<script th:inline="javascript">
const broadcaster = /*[[${broadcaster}]]*/ '';
</script>
<script src="/js/toast.js"></script>
<script src="/js/broadcast.js"></script>
</body>
</head>
<body class="broadcast-body">
<canvas id="broadcast-canvas"></canvas>
<script th:inline="javascript">
const broadcaster = /*[[${broadcaster}]]*/ "";
</script>
<script src="/js/toast.js"></script>
<script src="/js/broadcast.js"></script>
</body>
</html>

View File

@@ -1,36 +1,44 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<head>
<meta charset="UTF-8" />
<title>Browse channels - Imgfloat</title>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body class="channels-body">
<div class="channels-shell">
<header class="channels-header">
</head>
<body class="channels-body">
<div class="channels-shell">
<header class="channels-header">
<div class="brand">
<div class="brand-mark">IF</div>
<div>
<div class="brand-title">Imgfloat</div>
<div class="brand-subtitle">Twitch overlay manager</div>
</div>
<div class="brand-mark">IF</div>
<div>
<div class="brand-title">Imgfloat</div>
<div class="brand-subtitle">Twitch overlay manager</div>
</div>
</div>
</header>
</header>
<main class="channels-main">
<main class="channels-main">
<section class="channel-card">
<p class="eyebrow subtle">Broadcast overlay</p>
<h1>Open a channel</h1>
<p class="muted">Type the channel name to jump straight to their overlay.</p>
<form id="channel-search-form" class="channel-form">
<label class="sr-only" for="channel-search">Channel name</label>
<input id="channel-search" name="channel" class="text-input" type="text" list="channel-suggestions" placeholder="Type a channel name" autocomplete="off" />
<datalist id="channel-suggestions"></datalist>
<button type="submit" class="button block">Open overlay</button>
</form>
<p class="eyebrow subtle">Broadcast overlay</p>
<h1>Open a channel</h1>
<p class="muted">Type the channel name to jump straight to their overlay.</p>
<form id="channel-search-form" class="channel-form">
<label class="sr-only" for="channel-search">Channel name</label>
<input
id="channel-search"
name="channel"
class="text-input"
type="text"
list="channel-suggestions"
placeholder="Type a channel name"
autocomplete="off"
/>
<datalist id="channel-suggestions"></datalist>
<button type="submit" class="button block">Open overlay</button>
</form>
</section>
</main>
</div>
<script src="/js/landing.js"></script>
</body>
</main>
</div>
<script src="/js/landing.js"></script>
</body>
</html>

View File

@@ -1,151 +1,166 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<head>
<meta charset="UTF-8" />
<title>Imgfloat Dashboard</title>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body class="dashboard-body">
<div class="dashboard-shell">
<header class="dashboard-topbar">
</head>
<body class="dashboard-body">
<div class="dashboard-shell">
<header class="dashboard-topbar">
<div class="brand">
<div class="brand-mark">IF</div>
<div>
<div class="brand-title">Imgfloat</div>
<div class="brand-subtitle">Twitch overlay manager</div>
</div>
<div class="brand-mark">IF</div>
<div>
<div class="brand-title">Imgfloat</div>
<div class="brand-subtitle">Twitch overlay manager</div>
</div>
</div>
<div class="user-pill">
<span class="eyebrow subtle">Signed in as</span>
<span class="user-display" th:text="${username}">user</span>
<span class="eyebrow subtle">Signed in as</span>
<span class="user-display" th:text="${username}">user</span>
</div>
</header>
</header>
<section class="card">
<section class="card">
<p class="eyebrow">Navigation</p>
<h3>Shortcuts</h3>
<p class="muted">Jump into your overlay</p>
<div class="panel-actions">
<a class="button block" th:href="@{'/view/' + ${channel} + '/broadcast'}">Open broadcast overlay</a>
<a class="button ghost block" th:href="@{'/view/' + ${channel} + '/admin'}">Open admin console</a>
<a class="button ghost block" href="/channels">Browse channels</a>
<form class="block" th:action="@{/logout}" method="post">
<button class="secondary block" type="submit">Logout</button>
</form>
<a class="button block" th:href="@{'/view/' + ${channel} + '/broadcast'}">Open broadcast overlay</a>
<a class="button ghost block" th:href="@{'/view/' + ${channel} + '/admin'}">Open admin console</a>
<a class="button ghost block" href="/channels">Browse channels</a>
<form class="block" th:action="@{/logout}" method="post">
<button class="secondary block" type="submit">Logout</button>
</form>
</div>
</section>
</section>
<section class="card">
<section class="card">
<p class="eyebrow">Settings</p>
<h3>Overlay dimensions</h3>
<p class="muted">Match these with your OBS resolution.</p>
<div class="control-grid">
<label>
Width
<input id="canvas-width" type="number" min="100" step="10" />
</label>
<label>
Height
<input id="canvas-height" type="number" min="100" step="10" />
</label>
<label>
Width
<input id="canvas-width" type="number" min="100" step="10" />
</label>
<label>
Height
<input id="canvas-height" type="number" min="100" step="10" />
</label>
</div>
<div class="control-actions">
<button type="button" onclick="saveCanvasSettings()">Save canvas size</button>
<span id="canvas-status" class="muted"></span>
<button type="button" onclick="saveCanvasSettings()">Save canvas size</button>
<span id="canvas-status" class="muted"></span>
</div>
</section>
</section>
<section class="card-grid two-col">
<section class="card-grid two-col">
<div class="card">
<div class="card-header">
<div>
<p class="eyebrow">Collaboration</p>
<h3>Channel admins</h3>
<p class="muted">Invite moderators to help manage assets.</p>
</div>
</div>
<div class="inline-form">
<input id="new-admin" placeholder="Twitch username" />
<button type="button" onclick="addAdmin()">Add admin</button>
</div>
<div class="card-section">
<div class="section-header">
<h4 class="list-title">Channel Admins</h4>
<p class="muted">Users who can currently modify your overlay.</p>
</div>
<ul id="admin-list" class="stacked-list"></ul>
</div>
<div class="card-section">
<div class="section-header">
<h4 class="list-title">Your Twitch moderators</h4>
<p class="muted">Add moderators who already help run your channel.</p>
</div>
<ul id="admin-suggestions" class="stacked-list"></ul>
</div>
</div>
</section>
<section th:if="${adminChannels != null}" class="card">
<div class="card-header">
<div class="card-header">
<div>
<p class="eyebrow">Your access</p>
<h3>Channels you administer</h3>
<p class="muted">Jump into a teammate's overlay console.</p>
<p class="eyebrow">Collaboration</p>
<h3>Channel admins</h3>
<p class="muted">Invite moderators to help manage assets.</p>
</div>
</div>
<div class="inline-form">
<input id="new-admin" placeholder="Twitch username" />
<button type="button" onclick="addAdmin()">Add admin</button>
</div>
<div class="card-section">
<div class="section-header">
<h4 class="list-title">Channel Admins</h4>
<p class="muted">Users who can currently modify your overlay.</p>
</div>
<ul id="admin-list" class="stacked-list"></ul>
</div>
<div class="card-section">
<div class="section-header">
<h4 class="list-title">Your Twitch moderators</h4>
<p class="muted">Add moderators who already help run your channel.</p>
</div>
<ul id="admin-suggestions" class="stacked-list"></ul>
</div>
</div>
</section>
<section th:if="${adminChannels != null}" class="card">
<div class="card-header">
<div>
<p class="eyebrow">Your access</p>
<h3>Channels you administer</h3>
<p class="muted">Jump into a teammate's overlay console.</p>
</div>
</div>
<p th:if="${#lists.isEmpty(adminChannels)}">No admin invitations yet.</p>
<ul th:if="${!#lists.isEmpty(adminChannels)}" class="stacked-list">
<li th:each="channelName : ${adminChannels}" class="stacked-list-item">
<div>
<p class="list-title" th:text="${channelName}">channel</p>
<p class="muted">Channel admin access</p>
</div>
<a class="button ghost" th:href="@{'/view/' + ${channelName} + '/admin'}">Open</a>
</li>
</ul>
</section>
<section class="card download-card-block">
<div class="download-header">
<li th:each="channelName : ${adminChannels}" class="stacked-list-item">
<div>
<p class="eyebrow">Desktop app</p>
<h3>Download Imgfloat</h3>
<p class="muted">Version <span class="version-inline" th:text="${releaseVersion}">0.0.1</span> · build <span class="version-inline" th:text="${version}">unknown</span></p>
<p class="list-title" th:text="${channelName}">channel</p>
<p class="muted">Channel admin access</p>
</div>
<a class="button ghost" th:href="@{'/view/' + ${channelName} + '/admin'}">Open</a>
</li>
</ul>
</section>
<section class="card download-card-block">
<div class="download-header">
<div>
<p class="eyebrow">Desktop app</p>
<h3>Download Imgfloat</h3>
<p class="muted">
Version <span class="version-inline" th:text="${releaseVersion}">0.0.1</span> · build
<span class="version-inline" th:text="${version}">unknown</span>
</p>
</div>
</div>
<div class="download-grid">
<div class="download-card" data-platform="mac">
<div class="download-card-header">
<p class="eyebrow">macOS</p>
<span class="badge soft recommended-badge hidden">Recommended</span>
</div>
<p class="muted">Apple Silicon build (ARM64)</p>
<a class="button block" th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '-arm64.dmg'">Download .dmg</a>
<div class="download-card" data-platform="mac">
<div class="download-card-header">
<p class="eyebrow">macOS</p>
<span class="badge soft recommended-badge hidden">Recommended</span>
</div>
<div class="download-card" data-platform="windows">
<div class="download-card-header">
<p class="eyebrow">Windows</p>
<span class="badge soft recommended-badge hidden">Recommended</span>
</div>
<p class="muted">Installer for Windows 10 and 11</p>
<a class="button block" th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat.Setup.' + ${releaseVersion} + '.exe'">Download .exe</a>
<p class="muted">Apple Silicon build (ARM64)</p>
<a
class="button block"
th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '-arm64.dmg'"
>Download .dmg</a
>
</div>
<div class="download-card" data-platform="windows">
<div class="download-card-header">
<p class="eyebrow">Windows</p>
<span class="badge soft recommended-badge hidden">Recommended</span>
</div>
<div class="download-card" data-platform="linux">
<div class="download-card-header">
<p class="eyebrow">Linux</p>
<span class="badge soft recommended-badge hidden">Recommended</span>
</div>
<p class="muted">AppImage for most distributions</p>
<a class="button block" th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '.AppImage'">Download AppImage</a>
<p class="muted">Installer for Windows 10 and 11</p>
<a
class="button block"
th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat.Setup.' + ${releaseVersion} + '.exe'"
>Download .exe</a
>
</div>
<div class="download-card" data-platform="linux">
<div class="download-card-header">
<p class="eyebrow">Linux</p>
<span class="badge soft recommended-badge hidden">Recommended</span>
</div>
<p class="muted">AppImage for most distributions</p>
<a
class="button block"
th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '.AppImage'"
>Download AppImage</a
>
</div>
</div>
</section>
</div>
<script src="/js/toast.js"></script>
<script src="/js/downloads.js"></script>
<script th:inline="javascript">
const broadcaster = /*[[${channel}]]*/ '';
</script>
<script src="/js/dashboard.js"></script>
</body>
</section>
</div>
<script src="/js/toast.js"></script>
<script src="/js/downloads.js"></script>
<script th:inline="javascript">
const broadcaster = /*[[${channel}]]*/ "";
</script>
<script src="/js/dashboard.js"></script>
</body>
</html>

View File

@@ -1,79 +1,95 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<head>
<meta charset="UTF-8" />
<title>Imgfloat - Twitch overlay</title>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body class="landing-body">
<div class="landing">
<header class="landing-header">
</head>
<body class="landing-body">
<div class="landing">
<header class="landing-header">
<div class="brand">
<div class="brand-mark">IF</div>
<div>
<div class="brand-title">Imgfloat</div>
<div class="brand-subtitle">Twitch overlay manager</div>
</div>
<div class="brand-mark">IF</div>
<div>
<div class="brand-title">Imgfloat</div>
<div class="brand-subtitle">Twitch overlay manager</div>
</div>
</div>
</header>
</header>
<main class="hero hero-compact">
<main class="hero hero-compact">
<div class="hero-text">
<p class="eyebrow">Overlay toolkit</p>
<h1>Keep your Twitch overlays tidy.</h1>
<p class="lead">Upload artwork, drop it into a shared dashboard, and stay in sync with your mods without clutter.</p>
<div class="cta-row">
<a class="button" href="/oauth2/authorization/twitch">Login with Twitch</a>
<a class="button ghost" href="/channels" rel="prefetch">Browse channels</a>
</div>
<p class="eyebrow">Overlay toolkit</p>
<h1>Keep your Twitch overlays tidy.</h1>
<p class="lead">
Upload artwork, drop it into a shared dashboard, and stay in sync with your mods without clutter.
</p>
<div class="cta-row">
<a class="button" href="/oauth2/authorization/twitch">Login with Twitch</a>
<a class="button ghost" href="/channels" rel="prefetch">Browse channels</a>
</div>
</div>
</main>
</main>
<section class="download-section">
<section class="download-section">
<div class="download-header">
<p class="eyebrow">Desktop app</p>
<h2>Download Imgfloat</h2>
<p class="muted">Version <span class="version-inline" th:text="${releaseVersion}">0.0.1</span> for Windows, macOS, and Linux.</p>
<p class="eyebrow">Desktop app</p>
<h2>Download Imgfloat</h2>
<p class="muted">
Version <span class="version-inline" th:text="${releaseVersion}">0.0.1</span> for Windows, macOS, and Linux.
</p>
</div>
<div class="download-grid">
<div class="download-card" data-platform="mac">
<div class="download-card-header">
<p class="eyebrow">macOS</p>
<span class="badge soft recommended-badge hidden">Recommended</span>
</div>
<p class="muted">Apple Silicon build (ARM64)</p>
<a class="button block" th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '-arm64.dmg'">Download .dmg</a>
<div class="download-card" data-platform="mac">
<div class="download-card-header">
<p class="eyebrow">macOS</p>
<span class="badge soft recommended-badge hidden">Recommended</span>
</div>
<div class="download-card" data-platform="windows">
<div class="download-card-header">
<p class="eyebrow">Windows</p>
<span class="badge soft recommended-badge hidden">Recommended</span>
</div>
<p class="muted">Installer for Windows 10 and 11</p>
<a class="button block" th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat.Setup.' + ${releaseVersion} + '.exe'">Download .exe</a>
<p class="muted">Apple Silicon build (ARM64)</p>
<a
class="button block"
th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '-arm64.dmg'"
>Download .dmg</a
>
</div>
<div class="download-card" data-platform="windows">
<div class="download-card-header">
<p class="eyebrow">Windows</p>
<span class="badge soft recommended-badge hidden">Recommended</span>
</div>
<div class="download-card" data-platform="linux">
<div class="download-card-header">
<p class="eyebrow">Linux</p>
<span class="badge soft recommended-badge hidden">Recommended</span>
</div>
<p class="muted">AppImage for most distributions</p>
<a class="button block" th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '.AppImage'">Download AppImage</a>
<p class="muted">Installer for Windows 10 and 11</p>
<a
class="button block"
th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat.Setup.' + ${releaseVersion} + '.exe'"
>Download .exe</a
>
</div>
<div class="download-card" data-platform="linux">
<div class="download-card-header">
<p class="eyebrow">Linux</p>
<span class="badge soft recommended-badge hidden">Recommended</span>
</div>
<p class="muted">AppImage for most distributions</p>
<a
class="button block"
th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '.AppImage'"
>Download AppImage</a
>
</div>
</div>
</section>
</section>
<footer class="landing-meta">
<footer class="landing-meta">
<div class="build-chip">
<span class="muted">License</span>
<span class="version-badge">MIT</span>
<span class="muted">License</span>
<span class="version-badge">MIT</span>
</div>
<div class="build-chip">
<span class="muted">Build</span>
<span class="version-badge" th:text="${version}">unknown</span>
<span class="muted">Build</span>
<span class="version-badge" th:text="${version}">unknown</span>
</div>
</footer>
</div>
<script src="/js/downloads.js"></script>
</body>
</footer>
</div>
<script src="/js/downloads.js"></script>
</body>
</html>

View File

@@ -1,234 +1,254 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Imgfloat Admin</title>
<link rel="stylesheet" href="/css/styles.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>
</head>
<body class="settings-body">
<div class="settings-shell">
<header class="settings-header">
<div class="brand">
<div class="brand-mark">IF</div>
<div>
<div class="brand-title">Imgfloat</div>
<div class="brand-subtitle">Twitch overlay manager</div>
</div>
</div>
</header>
<main class="settings-main">
<section class="settings-card settings-hero">
<div class="hero-copy">
<p class="eyebrow subtle">System administrator settings</p>
<h1>Application defaults</h1>
<p class="muted">
Configure overlay performance and audio guardrails for every channel using Imgfloat.
These settings are applied globally.
</p>
<div class="badge-row">
<span class="badge soft"><i class="fa-solid fa-gauge-high"></i> Performance tuned</span>
<span class="badge"><i class="fa-solid fa-cloud"></i> Server-wide</span>
<span class="badge subtle"><i class="fa-solid fa-gear"></i> Admin only</span>
</div>
</div>
<div class="stat-grid compact">
<div class="stat">
<p class="stat-label">Canvas FPS</p>
<p class="stat-value" id="stat-canvas-fps">--</p>
<p class="stat-subtitle">Longest side <span id="stat-canvas-size">--</span></p>
</div>
<div class="stat">
<p class="stat-label">Playback speed</p>
<p class="stat-value" id="stat-playback-range">--</p>
<p class="stat-subtitle">Applies to all animations</p>
</div>
<div class="stat">
<p class="stat-label">Audio pitch</p>
<p class="stat-value" id="stat-audio-range">--</p>
<p class="stat-subtitle">Fraction of original clip</p>
</div>
<div class="stat">
<p class="stat-label">Volume limits</p>
<p class="stat-value" id="stat-volume-range">--</p>
<p class="stat-subtitle">Keeps alerts comfortable</p>
</div>
</div>
</section>
<div class="settings-layout">
<section class="settings-card settings-panel">
<div class="section-heading">
<div>
<p class="eyebrow subtle">Overlay defaults</p>
<h2>Performance & audio budget</h2>
<p class="muted tiny">Tune the canvas and audio guardrails to keep overlays smooth and balanced.</p>
</div>
</div>
<form novalidate id="settings-form" class="settings-form">
<div class="form-section">
<div class="form-heading">
<p class="eyebrow subtle">Canvas</p>
<h3>Rendering budget</h3>
<p class="muted tiny">Match FPS and max dimensions to your streaming canvas for consistent overlays.</p>
</div>
<div class="control-grid split-row">
<label for="canvas-fps">Canvas FPS
<input
id="canvas-fps"
name="canvas-fps"
class="text-input"
type="text"
inputmode="numeric"
pattern="^[1-9]\d*$"
placeholder="60"
/>
</label>
<label for="canvas-size">Canvas max side length (pixels)
<input
id="canvas-size"
name="canvas-size"
class="text-input"
type="text"
inputmode="numeric"
pattern="^[1-9]\d*$"
placeholder="1920"
/>
</label>
</div>
<p class="field-hint">Use the longest edge of your OBS browser source to prevent stretching.</p>
</div>
<div class="form-section">
<div class="form-heading">
<p class="eyebrow subtle">Playback</p>
<h3>Animation speed limits</h3>
<p class="muted tiny">Bound default speeds between 0 and 1 so clips run predictably.</p>
</div>
<div class="control-grid split-row">
<label for="min-playback-speed">Min playback speed
<input
id="min-playback-speed"
name="min-playback-speed"
class="text-input"
type="text"
inputmode="decimal"
pattern="^(0(\.\d+)?|1(\.0+)?)$"
placeholder="0.5"
/>
</label>
<label for="max-playback-speed">Max playback speed
<input
id="max-playback-speed"
name="max-playback-speed"
class="text-input"
type="text"
inputmode="decimal"
pattern="^(0(\.\d+)?|1(\.0+)?)$"
placeholder="1.0"
/>
</label>
</div>
<p class="field-hint">Keep the maximum at 1.0 to avoid speeding overlays beyond their source frame rate.</p>
</div>
<div class="form-section">
<div class="form-heading">
<p class="eyebrow subtle">Audio</p>
<h3>Pitch & volume guardrails</h3>
<p class="muted tiny">Prevent harsh audio by bounding pitch and volume as fractions of the source.</p>
</div>
<div class="control-grid split-row">
<label for="min-audio-pitch">Min audio pitch
<input
id="min-audio-pitch"
name="min-audio-pitch"
class="text-input"
type="text"
inputmode="decimal"
pattern="^(0(\.\d+)?|1(\.0+)?)$"
placeholder="0.8"
/>
</label>
<label for="max-audio-pitch">Max audio pitch
<input
id="max-audio-pitch"
name="max-audio-pitch"
class="text-input"
type="text"
inputmode="decimal"
pattern="^(0(\.\d+)?|1(\.0+)?)$"
placeholder="1.0"
/>
</label>
</div>
<div class="control-grid split-row">
<label for="min-volume">Min volume
<input
id="min-volume"
name="min-volume"
class="text-input"
type="text"
inputmode="decimal"
pattern="^(0(\.\d+)?|1(\.0+)?)$"
placeholder="0.2"
/>
</label>
<label for="max-volume">Max volume
<input
id="max-volume"
name="max-volume"
class="text-input"
type="text"
inputmode="decimal"
pattern="^(0(\.\d+)?|1(\.0+)?)$"
placeholder="1.0"
/>
</label>
</div>
<p class="field-hint">Volume and pitch values are percentages of the original clip between 0 and 1.</p>
</div>
<div class="form-footer">
<p id="settings-status" class="status-chip">No changes yet.</p>
<button id="settings-submit-button" type="submit" class="button" disabled>Save settings</button>
</div>
</form>
</section>
<aside class="settings-sidebar">
<section class="settings-card info-card">
<p class="eyebrow subtle">Checklist</p>
<h3>Before you save</h3>
<ul class="hint-list">
<li>Match canvas dimensions to the OBS browser source you embed.</li>
<li>Use 3060 FPS for smoother overlays without overwhelming viewers.</li>
<li>Keep playback and pitch bounds between 0 and 1 to avoid distortion.</li>
<li>Lower the minimum volume if alerts feel too loud on stream.</li>
</ul>
</section>
<section class="settings-card info-card subtle">
<p class="eyebrow subtle">Heads up</p>
<h3>Global impact</h3>
<p class="muted tiny">Changes here update every channel immediately. Save carefully and confirm with your team.</p>
</section>
</aside>
</div>
</main>
<head>
<meta charset="UTF-8" />
<title>Imgfloat Admin</title>
<link rel="stylesheet" href="/css/styles.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>
</head>
<body class="settings-body">
<div class="settings-shell">
<header class="settings-header">
<div class="brand">
<div class="brand-mark">IF</div>
<div>
<div class="brand-title">Imgfloat</div>
<div class="brand-subtitle">Twitch overlay manager</div>
</div>
</div>
<script th:inline="javascript">
const serverRenderedSettings = /*[[${settingsJson}]]*/;
</script>
<script src="/js/settings.js"></script>
<script src="/js/toast.js"></script>
</body>
</header>
<main class="settings-main">
<section class="settings-card settings-hero">
<div class="hero-copy">
<p class="eyebrow subtle">System administrator settings</p>
<h1>Application defaults</h1>
<p class="muted">
Configure overlay performance and audio guardrails for every channel using Imgfloat. These settings are
applied globally.
</p>
<div class="badge-row">
<span class="badge soft"><i class="fa-solid fa-gauge-high"></i> Performance tuned</span>
<span class="badge"><i class="fa-solid fa-cloud"></i> Server-wide</span>
<span class="badge subtle"><i class="fa-solid fa-gear"></i> Admin only</span>
</div>
</div>
<div class="stat-grid compact">
<div class="stat">
<p class="stat-label">Canvas FPS</p>
<p class="stat-value" id="stat-canvas-fps">--</p>
<p class="stat-subtitle">Longest side <span id="stat-canvas-size">--</span></p>
</div>
<div class="stat">
<p class="stat-label">Playback speed</p>
<p class="stat-value" id="stat-playback-range">--</p>
<p class="stat-subtitle">Applies to all animations</p>
</div>
<div class="stat">
<p class="stat-label">Audio pitch</p>
<p class="stat-value" id="stat-audio-range">--</p>
<p class="stat-subtitle">Fraction of original clip</p>
</div>
<div class="stat">
<p class="stat-label">Volume limits</p>
<p class="stat-value" id="stat-volume-range">--</p>
<p class="stat-subtitle">Keeps alerts comfortable</p>
</div>
</div>
</section>
<div class="settings-layout">
<section class="settings-card settings-panel">
<div class="section-heading">
<div>
<p class="eyebrow subtle">Overlay defaults</p>
<h2>Performance & audio budget</h2>
<p class="muted tiny">Tune the canvas and audio guardrails to keep overlays smooth and balanced.</p>
</div>
</div>
<form novalidate id="settings-form" class="settings-form">
<div class="form-section">
<div class="form-heading">
<p class="eyebrow subtle">Canvas</p>
<h3>Rendering budget</h3>
<p class="muted tiny">
Match FPS and max dimensions to your streaming canvas for consistent overlays.
</p>
</div>
<div class="control-grid split-row">
<label for="canvas-fps"
>Canvas FPS
<input
id="canvas-fps"
name="canvas-fps"
class="text-input"
type="text"
inputmode="numeric"
pattern="^[1-9]\d*$"
placeholder="60"
/>
</label>
<label for="canvas-size"
>Canvas max side length (pixels)
<input
id="canvas-size"
name="canvas-size"
class="text-input"
type="text"
inputmode="numeric"
pattern="^[1-9]\d*$"
placeholder="1920"
/>
</label>
</div>
<p class="field-hint">Use the longest edge of your OBS browser source to prevent stretching.</p>
</div>
<div class="form-section">
<div class="form-heading">
<p class="eyebrow subtle">Playback</p>
<h3>Animation speed limits</h3>
<p class="muted tiny">Bound default speeds between 0 and 1 so clips run predictably.</p>
</div>
<div class="control-grid split-row">
<label for="min-playback-speed"
>Min playback speed
<input
id="min-playback-speed"
name="min-playback-speed"
class="text-input"
type="text"
inputmode="decimal"
pattern="^(0(\.\d+)?|1(\.0+)?)$"
placeholder="0.5"
/>
</label>
<label for="max-playback-speed"
>Max playback speed
<input
id="max-playback-speed"
name="max-playback-speed"
class="text-input"
type="text"
inputmode="decimal"
pattern="^(0(\.\d+)?|1(\.0+)?)$"
placeholder="1.0"
/>
</label>
</div>
<p class="field-hint">
Keep the maximum at 1.0 to avoid speeding overlays beyond their source frame rate.
</p>
</div>
<div class="form-section">
<div class="form-heading">
<p class="eyebrow subtle">Audio</p>
<h3>Pitch & volume guardrails</h3>
<p class="muted tiny">Prevent harsh audio by bounding pitch and volume as fractions of the source.</p>
</div>
<div class="control-grid split-row">
<label for="min-audio-pitch"
>Min audio pitch
<input
id="min-audio-pitch"
name="min-audio-pitch"
class="text-input"
type="text"
inputmode="decimal"
pattern="^(0(\.\d+)?|1(\.0+)?)$"
placeholder="0.8"
/>
</label>
<label for="max-audio-pitch"
>Max audio pitch
<input
id="max-audio-pitch"
name="max-audio-pitch"
class="text-input"
type="text"
inputmode="decimal"
pattern="^(0(\.\d+)?|1(\.0+)?)$"
placeholder="1.0"
/>
</label>
</div>
<div class="control-grid split-row">
<label for="min-volume"
>Min volume
<input
id="min-volume"
name="min-volume"
class="text-input"
type="text"
inputmode="decimal"
pattern="^(0(\.\d+)?|1(\.0+)?)$"
placeholder="0.2"
/>
</label>
<label for="max-volume"
>Max volume
<input
id="max-volume"
name="max-volume"
class="text-input"
type="text"
inputmode="decimal"
pattern="^(0(\.\d+)?|1(\.0+)?)$"
placeholder="1.0"
/>
</label>
</div>
<p class="field-hint">Volume and pitch values are percentages of the original clip between 0 and 1.</p>
</div>
<div class="form-footer">
<p id="settings-status" class="status-chip">No changes yet.</p>
<button id="settings-submit-button" type="submit" class="button" disabled>Save settings</button>
</div>
</form>
</section>
<aside class="settings-sidebar">
<section class="settings-card info-card">
<p class="eyebrow subtle">Checklist</p>
<h3>Before you save</h3>
<ul class="hint-list">
<li>Match canvas dimensions to the OBS browser source you embed.</li>
<li>Use 3060 FPS for smoother overlays without overwhelming viewers.</li>
<li>Keep playback and pitch bounds between 0 and 1 to avoid distortion.</li>
<li>Lower the minimum volume if alerts feel too loud on stream.</li>
</ul>
</section>
<section class="settings-card info-card subtle">
<p class="eyebrow subtle">Heads up</p>
<h3>Global impact</h3>
<p class="muted tiny">
Changes here update every channel immediately. Save carefully and confirm with your team.
</p>
</section>
</aside>
</div>
</main>
</div>
<script th:inline="javascript">
const serverRenderedSettings = /*[[${settingsJson}]]*/;
</script>
<script src="/js/settings.js"></script>
<script src="/js/toast.js"></script>
</body>
</html>