Merge pull request #3 from Spengreb/tv-layout

Add TV layout mode
This commit is contained in:
Spengreb 2026-04-21 00:11:45 +02:00 committed by GitHub
commit 80cd107aa0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 465 additions and 2 deletions

View file

@ -29,7 +29,9 @@ function checkAdmin(cb) {
*/
function handleAcp(req, res, _user) {
const ioServers = ioConfig.getSocketEndpoints();
const chosenServer = ioServers[0];
const preferredSecure = req.realProtocol === 'https';
const chosenServer = ioServers.find((server) => server.secure === preferredSecure) ||
ioServers[0];
if (!chosenServer) {
res.status(500).text("No suitable socket.io address for ACP");

View file

@ -24,7 +24,10 @@ export default function initialize(app, ioConfig, chanPath, getBannedChannel) {
if (endpoints.length === 0) {
throw new HTTPError('No socket.io endpoints configured');
}
const socketBaseURL = endpoints[0].url;
const preferredSecure = req.realProtocol === 'https';
const chosenEndpoint = endpoints.find((endpoint) => endpoint.secure === preferredSecure) ||
endpoints[0];
const socketBaseURL = chosenEndpoint.url;
sendPug(res, 'channel', {
channelName: req.params.channel,

View file

@ -20,8 +20,10 @@ html(lang="en")
a.dropdown-toggle(href="#", data-toggle="dropdown") Layout
b.caret
ul.dropdown-menu
li: a(href="#" onclick="javascript:backToNormalLayout(event); return false;") Back to Normal
li: a(href="#" onclick="javascript:chatOnly()") Chat Only
li: a(href="#" onclick="javascript:removeVideo(event)") Remove Video
li: a(href="#" onclick="javascript:tvLayout(event); return false;") TV / Big Picture
+navsuperadmin(true)
+navloginlogout()
section#mainpage

View file

@ -44,6 +44,7 @@ mixin us-general
option(value="fluid") Fluid
option(value="synchtube-fluid") Synchtube + Fluid
option(value="hd") HD
option(value="tv") TV / Big Picture
.col-sm-4
.col-sm-8
p.text-danger Changing layouts may require refreshing to take effect.

View file

@ -704,3 +704,179 @@ body.hd #resize-video-larger, body.hd #resize-video-smaller {
.userlist-ignored {
text-decoration: line-through;
}
/* === TV / Big Picture Mode === */
body.tv #mainpage > .container-fluid {
padding: 0;
}
body.tv #motdrow,
body.tv #announcements,
body.tv #drinkbarwrap {
display: none;
}
body.tv #main.row {
position: relative;
height: calc(100vh - 60px);
margin: 0;
overflow: hidden;
}
body.tv #videowrap {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100% !important;
height: 100% !important;
margin: 0;
padding: 0;
z-index: 1;
}
body.tv #videowrap-header {
display: none;
}
body.tv #videowrap .embed-responsive {
padding-bottom: 0 !important;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100% !important;
}
body.tv #videowrap .embed-responsive-item,
body.tv #videowrap iframe,
body.tv #videowrap video,
body.tv #videowrap object,
body.tv #videowrap .video-js {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
}
/* Chat overlay — left side, fully transparent background */
body.tv #chatwrap {
position: absolute;
top: 0;
left: 0;
width: 460px;
bottom: 50px;
background: transparent !important;
z-index: 10;
margin: 0;
padding: 8px;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
body.tv #chatheader {
display: none;
}
body.tv #userlist {
display: none;
}
body.tv #messagebuffer {
background: transparent !important;
border: none !important;
overflow: hidden !important;
height: 100% !important;
max-height: 100% !important;
margin: 0;
flex: 1 1 auto;
}
body.tv #messagebuffer > * {
background: rgba(0, 0, 0, 0.18) !important;
border-radius: 4px;
padding: 4px 8px;
margin-bottom: 6px;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.9);
}
body.tv #messagebuffer > *.tv-msg {
color: #fff;
opacity: 1;
font-size: 19px;
font-weight: 600;
line-height: 1.25;
letter-spacing: 0.01em;
transition: opacity 8s linear;
}
body.tv #messagebuffer > *.tv-msg .timestamp {
display: none !important;
}
body.tv #messagebuffer > *.tv-msg *,
body.tv #messagebuffer > *.tv-msg .username,
body.tv #messagebuffer > *.tv-msg a {
color: inherit !important;
}
body.tv #messagebuffer > *.tv-msg .username {
font-weight: 700;
}
body.tv #messagebuffer > *.tv-msg.tv-msg-fadeout {
opacity: 0;
}
/* Controls bar — hidden until mouse moves */
#tv-controls-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 50px;
background: rgba(0, 0, 0, 0.75);
z-index: 20;
display: flex;
align-items: center;
padding: 0 10px;
gap: 8px;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
body.tv-controls-visible #tv-controls-bar {
opacity: 1;
pointer-events: auto;
}
#tv-controls-bar form {
flex: 1;
margin: 0;
}
#tv-controls-bar #chatline {
border-radius: 4px;
margin: 0;
width: 100%;
}
#tv-controls-bar #guestlogin {
margin: 0;
}
/* Hide cursor when controls are not visible */
body.tv:not(.tv-controls-visible) {
cursor: none;
}
body.tv #controlsrow,
body.tv #playlistrow {
display: none;
}

View file

@ -746,6 +746,9 @@ function applyOpts() {
case "hd":
hdLayout();
break;
case "tv":
tvLayout();
break;
default:
compactLayout();
break;
@ -1584,6 +1587,9 @@ function addChatMessage(data) {
var msgBuf = $("#messagebuffer");
var div = formatChatMessage(data, LASTCHAT);
if ($("body").hasClass("tv")) {
decorateTVMessage(div[0], data.username);
}
// Incoming: a bunch of crap for the feature where if you hover over
// a message, it highlights messages from that user
var safeUsername = data.username.replace(/[^\w-]/g, '\\$');
@ -1597,6 +1603,10 @@ function addChatMessage(data) {
});
var oldHeight = msgBuf.prop("scrollHeight");
var numRemoved = trimChatBuffer();
if ($("body").hasClass("tv")) {
SCROLLCHAT = true;
$("#newmessages-indicator").remove();
}
if (SCROLLCHAT) {
scrollChat();
} else {
@ -1727,6 +1737,255 @@ function undoHDLayout() {
$("#messagebuffer, #userlist").css("max-height", "");
}
function tvUserMessageColor(username) {
var name = (username || "").toLowerCase();
var hash = 2166136261;
for (var i = 0; i < name.length; i++) {
hash ^= name.charCodeAt(i);
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
}
hash >>>= 0;
var hue = hash % 360;
var sat = 72 + ((hash >>> 9) % 24); // 72-95
var light = 58 + ((hash >>> 17) % 18); // 58-75
return "hsl(" + hue + ", " + sat + "%, " + light + "%)";
}
function clearTVMessageFadeTimer($msg) {
var timer = $msg.data("tvFadeTimer");
if (timer) {
clearTimeout(timer);
$msg.removeData("tvFadeTimer");
}
}
function detectCurrentLayoutName() {
var body = $("body");
if (body.hasClass("hd")) {
return "hd";
}
if (body.hasClass("synchtube") && body.hasClass("fluid")) {
return "synchtube-fluid";
}
if (body.hasClass("synchtube")) {
return "synchtube";
}
if (body.hasClass("fluid")) {
return "fluid";
}
return "default";
}
function backToNormalLayout(event) {
if (event) {
event.preventDefault();
}
var nextLayout = window._layoutBeforeTV;
if (!nextLayout || nextLayout === "tv") {
nextLayout = USEROPTS.layout;
}
if (!nextLayout || nextLayout === "tv") {
nextLayout = "fluid";
}
if ($("body").hasClass("tv")) {
undoTVLayout();
}
switch (nextLayout) {
case "synchtube-fluid":
fluidLayout();
synchtubeLayout();
break;
case "synchtube":
compactLayout();
synchtubeLayout();
break;
case "fluid":
fluidLayout();
break;
case "hd":
hdLayout();
break;
default:
compactLayout();
break;
}
window._layoutBeforeTV = null;
}
function scheduleTVMessageFade($msg) {
clearTVMessageFadeTimer($msg);
$msg.removeClass("tv-msg-fadeout");
var timer = setTimeout(function() {
if (!$("body").hasClass("tv")) {
return;
}
$msg.addClass("tv-msg-fadeout");
$msg.removeData("tvFadeTimer");
}, 8000);
$msg.data("tvFadeTimer", timer);
}
function decorateTVMessage(node, username) {
if (!node || node.nodeType !== 1) {
return;
}
var $msg = $(node);
var effectiveUsername = username || $msg.data("tvUsername");
if (!effectiveUsername) {
var nameText = $msg.find("strong.username").first().text() || "";
effectiveUsername = nameText.replace(/:\s*$/, "");
}
if (effectiveUsername) {
$msg.data("tvUsername", effectiveUsername);
}
$msg.addClass("tv-msg");
if (effectiveUsername) {
$msg.css("color", tvUserMessageColor(effectiveUsername));
}
var $name = $msg.find("strong.username").first();
if (!$name.length && effectiveUsername) {
$name = $("<strong/>").addClass("username tv-injected-username")
.prependTo($msg);
}
if ($name.length && effectiveUsername) {
$name.text(effectiveUsername + ": ");
}
scheduleTVMessageFade($msg);
}
function tvLayout(event) {
if (event) {
event.preventDefault();
}
if (!$("body").hasClass("tv")) {
window._layoutBeforeTV = detectCurrentLayoutName();
} else {
undoTVLayout();
}
if ($("body").hasClass("hd")) undoHDLayout();
if ($("body").hasClass("synchtube")) {
$("body").removeClass("synchtube");
$("#chatwrap").detach().insertBefore($("#videowrap"));
$("#leftcontrols").detach().insertBefore($("#rightcontrols"));
$("#leftpane").detach().insertBefore($("#rightpane"));
$("#userlist").css("float", "left");
}
if ($("body").hasClass("fluid")) {
$("body").removeClass("fluid");
$(".container-fluid").removeClass("container-fluid").addClass("container");
}
$("body").removeClass("compact").addClass("tv");
$(".container").removeClass("container").addClass("container-fluid");
$("footer .container-fluid").removeClass("container-fluid").addClass("container");
var $bar = $("<div/>").attr("id", "tv-controls-bar").appendTo($("#main"));
SCROLLCHAT = true;
$("#newmessages-indicator").remove();
$("#chatwrap form").detach().appendTo($bar);
$("#emotelistbtn").detach().appendTo($bar);
$("#videocontrols").detach().appendTo($bar);
$("#messagebuffer").children().each(function() {
decorateTVMessage(this);
});
var tvObserver = null;
if (typeof MutationObserver === "function") {
tvObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
for (var i = 0; i < mutation.addedNodes.length; i++) {
var node = mutation.addedNodes[i];
if (node.nodeType === 1) {
decorateTVMessage(node);
}
}
});
});
}
var buf = document.getElementById("messagebuffer");
if (buf && tvObserver) {
tvObserver.observe(buf, { childList: true });
}
var tvHideTimer = null;
function tvShowControls() {
$("body").addClass("tv-controls-visible");
clearTimeout(tvHideTimer);
tvHideTimer = setTimeout(function() {
if ($("body").hasClass("tv-typing")) {
return;
}
$("body").removeClass("tv-controls-visible");
window._tvHideTimer = null;
}, 3000);
window._tvHideTimer = tvHideTimer;
}
$(document).on("mousemove.tv touchstart.tv", tvShowControls);
$(document).on("focusin.tv", "#tv-controls-bar input, #tv-controls-bar textarea", function() {
$("body").addClass("tv-typing tv-controls-visible");
clearTimeout(tvHideTimer);
window._tvHideTimer = null;
});
$(document).on("focusout.tv", "#tv-controls-bar input, #tv-controls-bar textarea", function() {
if ($("#tv-controls-bar input:focus, #tv-controls-bar textarea:focus").length > 0) {
return;
}
$("body").removeClass("tv-typing");
tvShowControls();
});
tvShowControls();
window._tvObserver = tvObserver;
window._tvHideTimer = tvHideTimer;
}
function undoTVLayout() {
$("body").removeClass("tv tv-controls-visible tv-typing");
if (window._tvObserver) {
window._tvObserver.disconnect();
window._tvObserver = null;
}
clearTimeout(window._tvHideTimer);
window._tvHideTimer = null;
$(document).off("mousemove.tv touchstart.tv focusin.tv focusout.tv");
$("#messagebuffer").children(".tv-msg").each(function() {
var $msg = $(this);
clearTVMessageFadeTimer($msg);
$msg.removeClass("tv-msg tv-msg-fadeout");
$msg.css("color", "");
});
$("#messagebuffer .tv-injected-username").remove();
$("#videocontrols").detach().appendTo("#rightcontrols");
$("#emotelistbtn").detach().insertAfter($("#newpollbtn"));
$("#tv-controls-bar form").detach().appendTo("#chatwrap");
$("#tv-controls-bar").remove();
$(".container-fluid").removeClass("container-fluid").addClass("container");
$("footer .container").removeClass("container").addClass("container-fluid");
}
function compactLayout() {
/* Undo synchtube layout */
if ($("body").hasClass("synchtube")) {
@ -1752,11 +2011,20 @@ function compactLayout() {
undoHDLayout();
}
/* Undo TV layout */
if ($("body").hasClass("tv")) {
undoTVLayout();
}
$("body").addClass("compact");
handleVideoResize();
}
function fluidLayout() {
if ($("body").hasClass("tv")) {
undoTVLayout();
}
if ($("body").hasClass("hd")) {
undoHDLayout();
}
@ -1767,6 +2035,10 @@ function fluidLayout() {
}
function synchtubeLayout() {
if ($("body").hasClass("tv")) {
undoTVLayout();
}
if ($("body").hasClass("hd")) {
undoHDLayout();
}
@ -1785,6 +2057,10 @@ function synchtubeLayout() {
* "HD" is kind of a misnomer. Should be renamed at some point.
*/
function hdLayout() {
if ($("body").hasClass("tv")) {
undoTVLayout();
}
var videowrap = $("#videowrap"),
chatwrap = $("#chatwrap"),
playlist = $("#rightpane")
@ -1882,6 +2158,8 @@ function handleWindowResize() {
$("#messagebuffer").outerHeight(h);
$("#userlist").outerHeight(h);
return;
} else if ($("body").hasClass("tv")) {
return;
} else {
handleVideoResize();
}
@ -1890,6 +2168,7 @@ function handleWindowResize() {
function handleVideoResize() {
if ($("#ytapiplayer").length === 0) return;
if ($("body").hasClass("tv")) return;
var intv, ticks = 0;
var resize = function () {