Additional configuration from environment

This commit is contained in:
2025-12-15 13:53:03 +01:00
parent 05c315a56f
commit 9932a350cc
8 changed files with 117 additions and 63 deletions

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2024 Imgfloat Copyright (c) 2025 Andreas Krühlmann
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -6,11 +6,23 @@
IMGFLOAT_DB_PATH ?= ./imgfloat.db IMGFLOAT_DB_PATH ?= ./imgfloat.db
IMGFLOAT_ASSETS_PATH ?= ./assets IMGFLOAT_ASSETS_PATH ?= ./assets
IMGFLOAT_PREVIEWS_PATH ?= ./previews IMGFLOAT_PREVIEWS_PATH ?= ./previews
IMGFLOAT_MAX_SPEED ?= 4.0
IMGFLOAT_MIN_AUDIO_SPEED ?= 0.1
IMGFLOAT_MAX_AUDIO_SPEED ?= 4.0
IMGFLOAT_MIN_AUDIO_PITCH ?= 0.5
IMGFLOAT_MAX_AUDIO_PITCH ?= 2.0
IMGFLOAT_MAX_AUDIO_VOLUME ?= 2.0
SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE ?= 10MB SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE ?= 10MB
SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE ?= 10MB SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE ?= 10MB
RUNTIME_ENV = IMGFLOAT_ASSETS_PATH=$(IMGFLOAT_ASSETS_PATH) \ RUNTIME_ENV = IMGFLOAT_ASSETS_PATH=$(IMGFLOAT_ASSETS_PATH) \
IMGFLOAT_PREVIEWS_PATH=$(IMGFLOAT_PREVIEWS_PATH) \ IMGFLOAT_PREVIEWS_PATH=$(IMGFLOAT_PREVIEWS_PATH) \
IMGFLOAT_DB_PATH=$(IMGFLOAT_DB_PATH) \ IMGFLOAT_DB_PATH=$(IMGFLOAT_DB_PATH) \
IMGFLOAT_MAX_SPEED=$(IMGFLOAT_MAX_SPEED) \
IMGFLOAT_MIN_AUDIO_SPEED=$(IMGFLOAT_MIN_AUDIO_SPEED) \
IMGFLOAT_MAX_AUDIO_SPEED=$(IMGFLOAT_MAX_AUDIO_SPEED) \
IMGFLOAT_MIN_AUDIO_PITCH=$(IMGFLOAT_MIN_AUDIO_PITCH) \
IMGFLOAT_MAX_AUDIO_PITCH=$(IMGFLOAT_MAX_AUDIO_PITCH) \
IMGFLOAT_MAX_AUDIO_VOLUME=$(IMGFLOAT_MAX_AUDIO_VOLUME) \
SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE=$(SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE) \ SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE=$(SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE) \
SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE=$(SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE) SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE=$(SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE)
WATCHDIR = ./src/main WATCHDIR = ./src/main

View File

@@ -28,6 +28,18 @@ public class SystemEnvironmentValidator {
private String previewsPath; private String previewsPath;
@Value("${IMGFLOAT_DB_PATH}") @Value("${IMGFLOAT_DB_PATH}")
private String dbPath; private String dbPath;
@Value("${IMGFLOAT_MAX_SPEED}")
private double maxSpeed;
@Value("${IMGFLOAT_MIN_AUDIO_SPEED}")
private double minAudioSpeed;
@Value("${IMGFLOAT_MAX_AUDIO_SPEED}")
private double maxAudioSpeed;
@Value("${IMGFLOAT_MIN_AUDIO_PITCH}")
private double minAudioPitch;
@Value("${IMGFLOAT_MAX_AUDIO_PITCH}")
private double maxAudioPitch;
@Value("${IMGFLOAT_MAX_AUDIO_VOLUME}")
private double maxAudioVolume;
private long maxUploadBytes; private long maxUploadBytes;
private long maxRequestBytes; private long maxRequestBytes;
@@ -38,8 +50,14 @@ public class SystemEnvironmentValidator {
maxUploadBytes = DataSize.parse(springMaxFileSize).toBytes(); maxUploadBytes = DataSize.parse(springMaxFileSize).toBytes();
maxRequestBytes = DataSize.parse(springMaxRequestSize).toBytes(); maxRequestBytes = DataSize.parse(springMaxRequestSize).toBytes();
checkLong(maxUploadBytes, "SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE", missing); checkUnsignedNumeric(maxUploadBytes, "SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE", missing);
checkLong(maxRequestBytes, "SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE", missing); checkUnsignedNumeric(maxRequestBytes, "SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE", missing);
checkUnsignedNumeric(maxSpeed, "IMGFLOAT_MAX_SPEED", missing);;
checkUnsignedNumeric(minAudioSpeed, "IMGFLOAT_MIN_AUDIO_SPEED", missing);;
checkUnsignedNumeric(maxAudioSpeed, "IMGFLOAT_MAX_AUDIO_SPEED", missing);;
checkUnsignedNumeric(minAudioPitch, "IMGFLOAT_MIN_AUDIO_PITCH", missing);;
checkUnsignedNumeric(maxAudioPitch, "IMGFLOAT_MAX_AUDIO_PITCH", missing);;
checkUnsignedNumeric(maxAudioVolume, "IMGFLOAT_MAX_AUDIO_VOLUME", missing);;
checkString(twitchClientId, "TWITCH_CLIENT_ID", missing); checkString(twitchClientId, "TWITCH_CLIENT_ID", missing);
checkString(dbPath, "IMGFLOAT_DB_PATH", missing); checkString(dbPath, "IMGFLOAT_DB_PATH", missing);
checkString(twitchClientSecret, "TWITCH_CLIENT_SECRET", missing); checkString(twitchClientSecret, "TWITCH_CLIENT_SECRET", missing);
@@ -57,15 +75,18 @@ public class SystemEnvironmentValidator {
log.info(" - TWITCH_CLIENT_ID: {}", redact(twitchClientId)); log.info(" - TWITCH_CLIENT_ID: {}", redact(twitchClientId));
log.info(" - TWITCH_CLIENT_SECRET: {}", redact(twitchClientSecret)); log.info(" - TWITCH_CLIENT_SECRET: {}", redact(twitchClientSecret));
log.info(" - SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE: {} ({} bytes)", log.info(" - SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE: {} ({} bytes)",
springMaxFileSize, springMaxFileSize, maxUploadBytes);
maxUploadBytes
);
log.info(" - SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE: {} ({} bytes)", log.info(" - SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE: {} ({} bytes)",
springMaxRequestSize, springMaxRequestSize, maxRequestBytes);
maxRequestBytes
);
log.info(" - IMGFLOAT_ASSETS_PATH: {}", assetsPath); log.info(" - IMGFLOAT_ASSETS_PATH: {}", assetsPath);
log.info(" - IMGFLOAT_PREVIEWS_PATH: {}", previewsPath); log.info(" - IMGFLOAT_PREVIEWS_PATH: {}", previewsPath);
log.info(" - IMGFLOAT_DB_PATH: {}", dbPath);
log.info(" - IMGFLOAT_MAX_SPEED: {}", maxSpeed);
log.info(" - IMGFLOAT_MIN_AUDIO_SPEED: {}", minAudioSpeed);
log.info(" - IMGFLOAT_MAX_AUDIO_SPEED: {}", maxAudioSpeed);
log.info(" - IMGFLOAT_MIN_AUDIO_PITCH: {}", minAudioPitch);
log.info(" - IMGFLOAT_MAX_AUDIO_PITCH: {}", maxAudioPitch);
log.info(" - IMGFLOAT_MAX_AUDIO_VOLUME: {}", maxAudioVolume);
} }
private void checkString(String value, String name, StringBuilder missing) { private void checkString(String value, String name, StringBuilder missing) {
@@ -74,44 +95,15 @@ public class SystemEnvironmentValidator {
} }
} }
private void checkLong(Long value, String name, StringBuilder missing) { private <T extends Number> void checkUnsignedNumeric(T value, String name, StringBuilder missing) {
if (value == null || value <= 0) { if (value == null || value.doubleValue() <= 0) {
missing.append(" - ").append(name).append("\n"); missing.append(" - ").append(name).append('\n');
} }
} }
private String formatBytes(long bytes) {
if (bytes < 1024) return bytes + " B";
double kb = bytes / 1024.0;
if (kb < 1024) return String.format("%.2f KB", kb);
double mb = kb / 1024.0;
if (mb < 1024) return String.format("%.2f MB", mb);
double gb = mb / 1024.0;
return String.format("%.2f GB", gb);
}
private String redact(String value) { private String redact(String value) {
if (!StringUtils.hasText(value)) return "(missing)"; if (!StringUtils.hasText(value)) return "(missing)";
if (value.length() <= 6) return "******"; if (value.length() <= 6) return "******";
return value.substring(0, 2) + "****" + value.substring(value.length() - 2); return value.substring(0, 2) + "****" + value.substring(value.length() - 2);
} }
private long parseSizeToBytes(String value) {
if (value == null) return -1;
String v = value.trim().toUpperCase(Locale.ROOT);
try {
if (v.endsWith("GB")) return Long.parseLong(v.replace("GB", "")) * 1024 * 1024 * 1024;
if (v.endsWith("MB")) return Long.parseLong(v.replace("MB", "")) * 1024 * 1024;
if (v.endsWith("KB")) return Long.parseLong(v.replace("KB", "")) * 1024;
if (v.endsWith("B")) return Long.parseLong(v.replace("B", ""));
return Long.parseLong(v);
} catch (NumberFormatException e) {
return -1;
}
}
} }

View File

@@ -5,6 +5,7 @@ import dev.kruhlmann.imgfloat.service.VersionService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
@@ -22,9 +23,31 @@ public class ViewController {
@Autowired @Autowired
private long uploadLimitBytes; private long uploadLimitBytes;
public ViewController(ChannelDirectoryService channelDirectoryService, VersionService versionService) { private double maxSpeed;
private double minAudioSpeed;
private double maxAudioSpeed;
private double minAudioPitch;
private double maxAudioPitch;
private double maxAudioVolume;
public ViewController(
ChannelDirectoryService channelDirectoryService,
VersionService versionService,
@Value("${IMGFLOAT_MAX_SPEED}") double maxSpeed,
@Value("${IMGFLOAT_MIN_AUDIO_SPEED}") double minAudioSpeed,
@Value("${IMGFLOAT_MAX_AUDIO_SPEED}") double maxAudioSpeed,
@Value("${IMGFLOAT_MIN_AUDIO_PITCH}") double minAudioPitch,
@Value("${IMGFLOAT_MAX_AUDIO_PITCH}") double maxAudioPitch,
@Value("${IMGFLOAT_MAX_AUDIO_VOLUME}") double maxAudioVolume
) {
this.channelDirectoryService = channelDirectoryService; this.channelDirectoryService = channelDirectoryService;
this.versionService = versionService; this.versionService = versionService;
this.maxSpeed = maxSpeed;
this.minAudioSpeed = minAudioSpeed;
this.maxAudioSpeed = maxAudioSpeed;
this.minAudioPitch = minAudioPitch;
this.maxAudioPitch = maxAudioPitch;
this.maxAudioVolume = maxAudioVolume;
} }
@org.springframework.web.bind.annotation.GetMapping("/") @org.springframework.web.bind.annotation.GetMapping("/")
@@ -62,6 +85,13 @@ public class ViewController {
model.addAttribute("username", login); model.addAttribute("username", login);
model.addAttribute("uploadLimitBytes", uploadLimitBytes); model.addAttribute("uploadLimitBytes", uploadLimitBytes);
model.addAttribute("maxSpeed", maxSpeed);
model.addAttribute("minAudioSpeed", minAudioSpeed);
model.addAttribute("maxAudioSpeed", maxAudioSpeed);
model.addAttribute("minAudioPitch", minAudioPitch);
model.addAttribute("maxAudioPitch", maxAudioPitch);
model.addAttribute("maxAudioVolume", maxAudioVolume);
return "admin"; return "admin";
} }

View File

@@ -35,12 +35,6 @@ import static org.springframework.http.HttpStatus.PAYLOAD_TOO_LARGE;
@Service @Service
public class ChannelDirectoryService { public class ChannelDirectoryService {
private static final Logger logger = LoggerFactory.getLogger(ChannelDirectoryService.class); private static final Logger logger = LoggerFactory.getLogger(ChannelDirectoryService.class);
private static final double MAX_SPEED = 4.0;
private static final double MIN_AUDIO_SPEED = 0.1;
private static final double MAX_AUDIO_SPEED = 4.0;
private static final double MIN_AUDIO_PITCH = 0.5;
private static final double MAX_AUDIO_PITCH = 2.0;
private static final double MAX_AUDIO_VOLUME = 1.0;
private static final Pattern SAFE_FILENAME = Pattern.compile("[^a-zA-Z0-9._ -]"); private static final Pattern SAFE_FILENAME = Pattern.compile("[^a-zA-Z0-9._ -]");
private final ChannelRepository channelRepository; private final ChannelRepository channelRepository;
@@ -53,13 +47,26 @@ public class ChannelDirectoryService {
@Autowired @Autowired
private long uploadLimitBytes; private long uploadLimitBytes;
private double maxSpeed;
private double minAudioSpeed;
private double maxAudioSpeed;
private double minAudioPitch;
private double maxAudioPitch;
private double maxAudioVolume;
public ChannelDirectoryService( public ChannelDirectoryService(
ChannelRepository channelRepository, ChannelRepository channelRepository,
AssetRepository assetRepository, AssetRepository assetRepository,
SimpMessagingTemplate messagingTemplate, SimpMessagingTemplate messagingTemplate,
AssetStorageService assetStorageService, AssetStorageService assetStorageService,
MediaDetectionService mediaDetectionService, MediaDetectionService mediaDetectionService,
MediaOptimizationService mediaOptimizationService MediaOptimizationService mediaOptimizationService,
@Value("${IMGFLOAT_MAX_SPEED}") double maxSpeed,
@Value("${IMGFLOAT_MIN_AUDIO_SPEED}") double minAudioSpeed,
@Value("${IMGFLOAT_MAX_AUDIO_SPEED}") double maxAudioSpeed,
@Value("${IMGFLOAT_MIN_AUDIO_PITCH}") double minAudioPitch,
@Value("${IMGFLOAT_MAX_AUDIO_PITCH}") double maxAudioPitch,
@Value("${IMGFLOAT_MAX_AUDIO_VOLUME}") double maxAudioVolume
) { ) {
this.channelRepository = channelRepository; this.channelRepository = channelRepository;
this.assetRepository = assetRepository; this.assetRepository = assetRepository;
@@ -67,6 +74,12 @@ public class ChannelDirectoryService {
this.assetStorageService = assetStorageService; this.assetStorageService = assetStorageService;
this.mediaDetectionService = mediaDetectionService; this.mediaDetectionService = mediaDetectionService;
this.mediaOptimizationService = mediaOptimizationService; this.mediaOptimizationService = mediaOptimizationService;
this.maxSpeed = maxSpeed;
this.minAudioSpeed = minAudioSpeed;
this.maxAudioSpeed = maxAudioSpeed;
this.minAudioPitch = minAudioPitch;
this.maxAudioPitch = maxAudioPitch;
this.maxAudioVolume = maxAudioVolume;
} }
@@ -244,17 +257,17 @@ public class ChannelDirectoryService {
private void validateTransform(TransformRequest req) { private void validateTransform(TransformRequest req) {
if (req.getWidth() <= 0) throw new ResponseStatusException(BAD_REQUEST, "Width must be > 0"); if (req.getWidth() <= 0) throw new ResponseStatusException(BAD_REQUEST, "Width must be > 0");
if (req.getHeight() <= 0) throw new ResponseStatusException(BAD_REQUEST, "Height must be > 0"); if (req.getHeight() <= 0) throw new ResponseStatusException(BAD_REQUEST, "Height must be > 0");
if (req.getSpeed() != null && (req.getSpeed() < 0 || req.getSpeed() > MAX_SPEED)) if (req.getSpeed() != null && (req.getSpeed() < 0 || req.getSpeed() > maxSpeed))
throw new ResponseStatusException(BAD_REQUEST, "Speed must be between 0 and " + MAX_SPEED); throw new ResponseStatusException(BAD_REQUEST, "Speed must be between 0 and " + maxSpeed);
if (req.getZIndex() != null && req.getZIndex() < 1) if (req.getZIndex() != null && req.getZIndex() < 1)
throw new ResponseStatusException(BAD_REQUEST, "zIndex must be >= 1"); throw new ResponseStatusException(BAD_REQUEST, "zIndex must be >= 1");
if (req.getAudioDelayMillis() != null && req.getAudioDelayMillis() < 0) if (req.getAudioDelayMillis() != null && req.getAudioDelayMillis() < 0)
throw new ResponseStatusException(BAD_REQUEST, "Audio delay >= 0"); throw new ResponseStatusException(BAD_REQUEST, "Audio delay >= 0");
if (req.getAudioSpeed() != null && (req.getAudioSpeed() < MIN_AUDIO_SPEED || req.getAudioSpeed() > MAX_AUDIO_SPEED)) if (req.getAudioSpeed() != null && (req.getAudioSpeed() < minAudioSpeed || req.getAudioSpeed() > maxAudioSpeed))
throw new ResponseStatusException(BAD_REQUEST, "Audio speed out of range"); throw new ResponseStatusException(BAD_REQUEST, "Audio speed out of range");
if (req.getAudioPitch() != null && (req.getAudioPitch() < MIN_AUDIO_PITCH || req.getAudioPitch() > MAX_AUDIO_PITCH)) if (req.getAudioPitch() != null && (req.getAudioPitch() < minAudioPitch || req.getAudioPitch() > maxAudioPitch))
throw new ResponseStatusException(BAD_REQUEST, "Audio pitch out of range"); throw new ResponseStatusException(BAD_REQUEST, "Audio pitch out of range");
if (req.getAudioVolume() != null && (req.getAudioVolume() < 0 || req.getAudioVolume() > MAX_AUDIO_VOLUME)) if (req.getAudioVolume() != null && (req.getAudioVolume() < 0 || req.getAudioVolume() > maxAudioVolume))
throw new ResponseStatusException(BAD_REQUEST, "Audio volume out of range"); throw new ResponseStatusException(BAD_REQUEST, "Audio volume out of range");
} }

View File

@@ -22,8 +22,8 @@ let interactionState = null;
let lastSizeInputChanged = null; let lastSizeInputChanged = null;
const HANDLE_SIZE = 10; const HANDLE_SIZE = 10;
const ROTATE_HANDLE_OFFSET = 32; const ROTATE_HANDLE_OFFSET = 32;
const MAX_VOLUME = 2; const MAX_VOLUME = adminInputRestrictions.MAX_AUDIO_VOLUME;
const VOLUME_SLIDER_MAX = 200; const VOLUME_SLIDER_MAX = adminInputRestrictions.MAX_AUDIO_VOLUME * 100;
const VOLUME_CURVE_STRENGTH = -0.6; const VOLUME_CURVE_STRENGTH = -0.6;
const pendingTransformSaves = new Map(); const pendingTransformSaves = new Map();
const KEYBOARD_NUDGE_STEP = 5; const KEYBOARD_NUDGE_STEP = 5;
@@ -2023,8 +2023,8 @@ function uploadAsset(file = null) {
showToast('Choose an image, GIF, video, or audio file to upload.', 'info'); showToast('Choose an image, GIF, video, or audio file to upload.', 'info');
return; return;
} }
if (selectedFile.size > upload_limit_bytes) { if (selectedFile.size > adminInputRestrictions.UPLOAD_MAX_BYTES) {
showToast(`File is too large. Maximum upload size is ${upload_limit_bytes / 1024 / 1024} MB.`, 'error'); showToast(`File is too large. Maximum upload size is ${adminInputRestrictions.UPLOAD_MAX_BYTES / 1024 / 1024} MB.`, 'error');
return; return;
} }

View File

@@ -5,7 +5,6 @@
<title>Imgfloat Admin</title> <title>Imgfloat Admin</title>
<link rel="stylesheet" href="/css/styles.css" /> <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://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script> <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/stompjs@2.3.3/lib/stomp.min.js"></script>
</head> </head>
@@ -14,7 +13,7 @@
<header class="admin-topbar"> <header class="admin-topbar">
<div class="topbar-left"> <div class="topbar-left">
<div class="admin-identity"> <div class="admin-identity">
<p class="eyebrow subtle">Admin</p> <p class="eyebrow subtle">CHANNEL ADMIN</p>
<h1 th:text="${broadcaster}"></h1> <h1 th:text="${broadcaster}"></h1>
</div> </div>
</div> </div>
@@ -208,7 +207,15 @@
<script th:inline="javascript"> <script th:inline="javascript">
const broadcaster = /*[[${broadcaster}]]*/ ''; const broadcaster = /*[[${broadcaster}]]*/ '';
const username = /*[[${username}]]*/ ''; const username = /*[[${username}]]*/ '';
const upload_limit_bytes = /*[[${uploadLimitBytes}]]*/ + 0; const adminInputRestrictions = {
UPLOAD_MAX_BYTES: /*[[${uploadLimitBytes}]]*/ + 0,
MAX_SPEED: /*[[${maxSpeed}]]*/ + 0.0,
MIN_AUDIO_SPEED: /*[[${minAudioSpeed}]]*/ + 0.0,
MAX_AUDIO_SPEED: /*[[${maxAudioSpeed}]]*/ + 0.0,
MIN_AUDIO_PITCH: /*[[${minAudioPitch}]]*/ + 0.0,
MAX_AUDIO_PITCH: /*[[${maxAudioPitch}]]*/ + 0.0,
MAX_AUDIO_VOLUME: /*[[${maxAudioVolume}]]*/ + 0.0,
}
</script> </script>
<script src="/js/toast.js"></script> <script src="/js/toast.js"></script>
<script src="/js/admin.js"></script> <script src="/js/admin.js"></script>

View File

@@ -69,7 +69,7 @@
</div> </div>
<div class="card-section"> <div class="card-section">
<div class="section-header"> <div class="section-header">
<h4 class="list-title">Admins</h4> <h4 class="list-title">Channel Admins</h4>
<p class="muted">Users who can currently modify your overlay.</p> <p class="muted">Users who can currently modify your overlay.</p>
</div> </div>
<ul id="admin-list" class="stacked-list"></ul> <ul id="admin-list" class="stacked-list"></ul>
@@ -97,7 +97,7 @@
<li th:each="channelName : ${adminChannels}" class="stacked-list-item"> <li th:each="channelName : ${adminChannels}" class="stacked-list-item">
<div> <div>
<p class="list-title" th:text="${channelName}">channel</p> <p class="list-title" th:text="${channelName}">channel</p>
<p class="muted">Admin access</p> <p class="muted">Channel admin access</p>
</div> </div>
<a class="button ghost" th:href="@{'/view/' + ${channelName} + '/admin'}">Open</a> <a class="button ghost" th:href="@{'/view/' + ${channelName} + '/admin'}">Open</a>
</li> </li>