diff --git a/src/web/acp.js b/src/web/acp.js index ff5b896f..d12be30f 100644 --- a/src/web/acp.js +++ b/src/web/acp.js @@ -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"); diff --git a/src/web/routes/channel.js b/src/web/routes/channel.js index 89be413c..807aefde 100644 --- a/src/web/routes/channel.js +++ b/src/web/routes/channel.js @@ -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, diff --git a/templates/channel.pug b/templates/channel.pug index 1803b8ac..54e8d1ed 100644 --- a/templates/channel.pug +++ b/templates/channel.pug @@ -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 diff --git a/templates/useroptions.pug b/templates/useroptions.pug index 68f04bf3..d49e6010 100644 --- a/templates/useroptions.pug +++ b/templates/useroptions.pug @@ -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. diff --git a/www/css/cytube.css b/www/css/cytube.css index 65f5058e..719340cd 100644 --- a/www/css/cytube.css +++ b/www/css/cytube.css @@ -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; +} diff --git a/www/js/util.js b/www/js/util.js index f2ab21a7..8f31603b 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -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 = $("").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 = $("
").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 () {