From 6281b0ead399b63c0bedb8f39170c67862617a86 Mon Sep 17 00:00:00 2001 From: Speng Reb Date: Tue, 21 Apr 2026 17:51:03 +0200 Subject: [PATCH] Emote compact and infinite scroll emote list --- www/css/cytube.css | 63 +++++++++++++++++++------------ www/js/ui.js | 94 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 129 insertions(+), 28 deletions(-) diff --git a/www/css/cytube.css b/www/css/cytube.css index 5159f72d..98f8c01c 100644 --- a/www/css/cytube.css +++ b/www/css/cytube.css @@ -604,34 +604,51 @@ table td { border-top-width: 0; } -.emotelist-table { - margin: auto; -} +.emotelist-table { margin: auto; } +.emote-preview-container { width: 60px; height: 60px; float: left; text-align: center; white-space: nowrap; margin: 3px; } +.emote-preview-hax { display: inline-block; vertical-align: middle; height: 100%; } +.emote-preview { max-width: 56px; max-height: 56px; cursor: pointer; } +.emotelist-paginator-container { text-align: center; } -.emote-preview-container { - width: 100px; - height: 100px; - float: left; - text-align: center; - white-space: nowrap; - margin: 5px; +/* Emote browser panel */ +#emote-browser { + display: none; + position: fixed; + z-index: 1060; + width: 340px; + background: #272b30; + border: 1px solid #555; + border-radius: 4px; + box-shadow: 0 4px 14px rgba(0,0,0,.6); + padding: 6px; } - -.emote-preview-hax { - display: inline-block; - vertical-align: middle; - height: 100%; +#emote-browser-search { + width: 100%; + margin-bottom: 6px; + background: #1a1d20; + color: #c8c8c8; + border-color: #555; } - -.emote-preview { - max-width: 100px; - max-height: 100px; +#emote-browser-search:focus { border-color: #888; outline: none; box-shadow: none; } +#emote-browser-grid { + display: flex; + flex-wrap: wrap; + gap: 3px; + max-height: 320px; + overflow-y: auto; +} +.emote-browser-item { + width: 52px; + height: 52px; + display: flex; + align-items: center; + justify-content: center; cursor: pointer; + border-radius: 3px; + flex-shrink: 0; } - -.emotelist-paginator-container { - text-align: center; -} +.emote-browser-item:hover { background: #3a3f44; } +.emote-browser-item img { max-width: 48px; max-height: 48px; object-fit: contain; } #leftcontrols .btn { margin-right: 5px; diff --git a/www/js/ui.js b/www/js/ui.js index b7ded81b..4717438f 100644 --- a/www/js/ui.js +++ b/www/js/ui.js @@ -947,17 +947,101 @@ applyOpts(); })(); var EMOTELISTMODAL = $("#emotelist"); -$("#emotelistbtn").on('click', function () { - EMOTELISTMODAL.modal(); -}); - -EMOTELISTMODAL.on('shown.bs.modal', function () { $('.emotelist-search').trigger('focus') }); EMOTELISTMODAL.find(".emotelist-alphabetical").change(function () { USEROPTS.emotelist_sort = this.checked; setOpt("emotelist_sort", USEROPTS.emotelist_sort); }); EMOTELISTMODAL.find(".emotelist-alphabetical").prop("checked", USEROPTS.emotelist_sort); +/* emote browser panel */ +var EMOTE_BROWSER_OFFSET = 0; +var EMOTE_BROWSER_BATCH = 40; +var EMOTE_BROWSER_FILTER = ''; + +$('body').append( + '
' + + '' + + '
' + + '
' +); + +function emoteBrowserMatches() { + if (!CHANNEL.emotes) return []; + var f = EMOTE_BROWSER_FILTER.toLowerCase(); + return f ? CHANNEL.emotes.filter(function(e) { return e.name.toLowerCase().indexOf(f) !== -1; }) + : CHANNEL.emotes; +} + +function emoteBrowserRenderMore() { + var matches = emoteBrowserMatches(); + var end = Math.min(EMOTE_BROWSER_OFFSET + EMOTE_BROWSER_BATCH, matches.length); + var grid = document.getElementById('emote-browser-grid'); + for (var i = EMOTE_BROWSER_OFFSET; i < end; i++) { + (function(emote) { + var item = document.createElement('div'); + item.className = 'emote-browser-item'; + item.title = emote.name; + var img = document.createElement('img'); + img.src = emote.image; + item.appendChild(img); + item.addEventListener('click', function() { + var cl = document.getElementById('chatline'); + var val = cl.value; + if (val && !val.charAt(val.length - 1).match(/\s/)) val += ' '; + cl.value = val + emote.name; + $("#emote-browser").hide(); + cl.focus(); + }); + grid.appendChild(item); + })(matches[i]); + } + EMOTE_BROWSER_OFFSET = end; +} + +function emoteBrowserReset() { + EMOTE_BROWSER_OFFSET = 0; + document.getElementById('emote-browser-grid').innerHTML = ''; + emoteBrowserRenderMore(); +} + +function emoteBrowserPosition() { + var btn = $("#emotelistbtn"), off = btn.offset(); + var panel = $("#emote-browser"); + var pw = panel.outerWidth(), ph = panel.outerHeight(); + var ww = $(window).width(), wh = $(window).height(); + var left = off.left; + if (left + pw > ww - 8) left = Math.max(8, off.left + btn.outerWidth() - pw); + var top = off.top - ph - 4; + if (top < 8) top = off.top + btn.outerHeight() + 4; + panel.css({ top: top, left: left }); +} + +$("#emotelistbtn").on('click', function () { + var panel = $("#emote-browser"); + if (panel.is(':visible')) { panel.hide(); return; } + EMOTE_BROWSER_FILTER = ''; + $("#emote-browser-search").val(''); + emoteBrowserReset(); + panel.show(); + emoteBrowserPosition(); + document.getElementById('emote-browser-search').focus(); +}); + +$(document).on('click.emotebrowser', function (e) { + if (!$(e.target).closest('#emote-browser, #emotelistbtn').length) + $("#emote-browser").hide(); +}); + +$(document).on('input', '#emote-browser-search', function () { + EMOTE_BROWSER_FILTER = this.value; + emoteBrowserReset(); +}); + +document.getElementById('emote-browser-grid').addEventListener('scroll', function () { + if (this.scrollTop + this.clientHeight >= this.scrollHeight - 60) + emoteBrowserRenderMore(); +}); + $("#fullscreenbtn").on('click', function () { var elem = document.querySelector("#videowrap .embed-responsive"); // this shit is why frontend web development sucks