commit d7101033e729c84a7037623947cf1bbdb95d21e3 Author: Stephane Bouvard Date: Wed Jul 23 14:47:19 2025 +0200 Initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ + diff --git a/modules/alert-banner/alert-banner.js b/modules/alert-banner/alert-banner.js new file mode 100644 index 0000000..9d058eb --- /dev/null +++ b/modules/alert-banner/alert-banner.js @@ -0,0 +1,61 @@ +;(function() { + const moduleUrl = window.getModuleUrl(); + loadCSSModule('overlay-alertbanner-css', moduleUrl + '/css/alert-banner.css'); + + function initModule() { + const container = document.getElementById('mainContainer') || document.body; + if (!document.getElementById('alertBannerContainer')) { + const alertBannerContainer = document.createElement('div'); + alertBannerContainer.id = 'alertBannerContainer'; + + const alertBannerTitle = document.createElement('div'); + alertBannerTitle.id = 'alertBannerTitle'; + alertBannerTitle.innerHTML = '

'; + + const alertBannerMessage = document.createElement('div'); + alertBannerMessage.id = 'alertBannerMessage'; + alertBannerMessage.innerHTML = '

'; + + alertBannerContainer.appendChild(alertBannerTitle); + alertBannerContainer.appendChild(alertBannerMessage); + container.appendChild(alertBannerContainer); + } + } + initModule(); + + if (window.SBdispatcher) { + SBdispatcher.on('stream-alertbanner', data => { + showAlert(data.param1,data.param2); + }); + SBdispatcher.on('stream-alertbanner-hide', () => { + hideAlert(); + }); + } + + function showAlert(title = "" , message = "") { + const container = document.getElementById('alertBannerContainer'); + const alertTitle = document.getElementById('alertBannerTitle').querySelector('p'); + const alertText = document.getElementById('alertBannerMessage').querySelector('p'); + + if (title.length > 0) { + alertTitle.innerText = title; + } + if (message.length > 0) { + alertText.innerText = message; + } + + container._positionDivHandler = () => positionDiv(container); + container.addEventListener('animationend', container._positionDivHandler); + container.style.animation = "slide-in-right 2s ease forwards"; + + } + + function hideAlert() { + const container = document.getElementById('alertBannerContainer'); + container._positionDivHandler = () => positionDiv(container); + container.addEventListener('animationend', container._positionDivHandler); + container.style.animation = "slide-out-right 2s ease forwards"; + } + +})(); + diff --git a/modules/alert-banner/css/alert-banner.css b/modules/alert-banner/css/alert-banner.css new file mode 100644 index 0000000..35cbee6 --- /dev/null +++ b/modules/alert-banner/css/alert-banner.css @@ -0,0 +1,49 @@ +#alertBannerContainer { + z-index: 500; + position: fixed; + top: 30px; + left: auto; + right: -100%; + width: 25%; + height: 75px; + background: linear-gradient(to left, black, #00007f); + display: flex; + align-items: center; + overflow: hidden; + clip-path: polygon(30px 0, 100% 0, 100% 100%, 0 100%); +} +#alertBannerContainer #alertBannerTitle { + color: white; + font-family: 'Roboto', sans-serif; + font-weight: 700; + font-style: italic; + font-size: 26px; + text-decoration: underline; + white-space: nowrap; + left: 30px; + top: 0px; + position: absolute; +} +#alertBannerContainer #alertBannerTitle p { + margin: 0px; + padding: 0px 1em 0px 0px; + background: linear-gradient(to bottom, #ffff7f, #ffffff); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + color: transparent; +} +#alertBannerContainer #alertBannerMessage { + color: white; + font-family: 'Roboto', sans-serif; + font-weight: 700; + font-size: 36px; + white-space: nowrap; + left: 30px; + bottom: 0px; + position: absolute; +} +#alertBannerContainer #alertBannerMessage p { + margin: 0px; + padding: 0px 1em 0px 0px; +} diff --git a/modules/alert-banner/css/alert-banner.less b/modules/alert-banner/css/alert-banner.less new file mode 100644 index 0000000..fd59e96 --- /dev/null +++ b/modules/alert-banner/css/alert-banner.less @@ -0,0 +1,56 @@ +// out: alert-banner.css, sourcemap: false, compress: false + +#alertBannerContainer { + z-index: 500; + position: fixed; + top: 30px; + left: auto; + right: -100%; + width: 25%; + height: 75px; + background: linear-gradient(to left, black, #00007f); + display: flex; + align-items: center; + overflow: hidden; + clip-path: polygon( 30px 0, 100% 0, 100% 100%, 0 100% ); + + #alertBannerTitle { + color: white; + font-family: 'Roboto', sans-serif; + font-weight: 700; + font-style: italic; + font-size: 26px; + text-decoration: underline; + white-space: nowrap; + left: 30px; + top: 0px; + position: absolute; + + p { + margin: 0px; + padding: 0px 1em 0px 0px; + background: linear-gradient(to bottom, #ffff7f, #ffffff); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + color: transparent; + } + } + + #alertBannerMessage { + color: white; + font-family: 'Roboto', sans-serif; + font-weight: 700; + font-size: 36px; + white-space: nowrap; + left: 30px; + bottom: 0px; + position: absolute; + + p { + margin: 0px; + padding: 0px 1em 0px 0px; + } + } + +} diff --git a/modules/alert-video/alert-video.js b/modules/alert-video/alert-video.js new file mode 100644 index 0000000..e1f5d30 --- /dev/null +++ b/modules/alert-video/alert-video.js @@ -0,0 +1,114 @@ +;(function() { + const moduleUrl = window.getModuleUrl(); + loadCSSModule('overlay-alertvideo-css', moduleUrl + '/css/alert-video.css'); + + function initModule() { + const container = document.getElementById('mainContainer') || document.body; + if (!document.getElementById('alertVideoContainer')) { + const alertVideoContainer = document.createElement('div'); + alertVideoContainer.id = 'alertVideoContainer'; + container.appendChild(alertVideoContainer); + } + } + initModule(); + + if (window.SBdispatcher) { + SBdispatcher.on('stream-alert:Follow', data => { + showAlert({ + userName: data.user, + videoUrl: moduleUrl + '/files/ALERT_follow.webm', + delay: 12 + }); + }); + SBdispatcher.on('stream-alert:Sub', data => { + showAlert({ + userName: data.user, + videoUrl: moduleUrl + '/files/ALERT_sub.webm', + delay: 12 + }); + }); + SBdispatcher.on('stream-alert:SubGift', data => { + showAlert({ + userName: data.user, + videoUrl: moduleUrl + '/files/ALERT_gift.webm', + delay: 12, + topLine: 'Abonnement offert à ' + data.param1 + '', + // bottomLine: 'Il a déjà offert ' + data.param2 +' abonnements à la communauté !' + }); }); + } + + function showAlert({ userName, videoUrl, delay = 12, topLine = null, bottomLine = null }) { + const container = document.createElement('div'); + container.classList.add('alert_outer-container', 'playing'); + + const widget = document.createElement('div'); + widget.classList.add('alert_widget-container'); + + const textContainer = document.createElement('div'); + textContainer.classList.add('alert_text-container'); + + // Zone nom d’utilisateur (au centre de la vidéo) + const centerTextWrapper = document.createElement('div'); + const usernameText = document.createElement('p'); + usernameText.classList.add('alert_text'); + + const usernameSpan = document.createElement('span'); + usernameSpan.classList.add('alert_text-accent', 'alert_variable-username'); + usernameSpan.textContent = userName; + + usernameText.appendChild(usernameSpan); + centerTextWrapper.appendChild(usernameText); + + // Vidéo + const videoContainer = document.createElement('div'); + videoContainer.classList.add('alertVideoContainer'); + + const video = document.createElement('video'); + video.classList.add('alertVideo'); + video.autoplay = true; + video.muted = true; + video.playsInline = true; + + const source = document.createElement('source'); + source.src = videoUrl; + source.type = 'video/webm'; + video.appendChild(source); + videoContainer.appendChild(video); + + // Optionnel : ligne de texte en haut + if (topLine) { + const topTextContainer = document.createElement('div'); + topTextContainer.classList.add('alert_top-text-container'); + const topText = document.createElement('p'); + topText.classList.add('alert_top-text'); + topText.innerHTML = topLine; + topTextContainer.appendChild(topText); + container.appendChild(topTextContainer); + } + + // Optionnel : ligne de texte en bas + if (bottomLine) { + const bottomTextContainer = document.createElement('div'); + bottomTextContainer.classList.add('alert_bottom-text-container'); + const bottomText = document.createElement('p'); + bottomText.classList.add('alert_bottom-text'); + bottomText.innerHTML = bottomLine; + bottomTextContainer.appendChild(bottomText); + container.appendChild(bottomTextContainer); + } + + // Assemble tout + textContainer.appendChild(centerTextWrapper); + textContainer.appendChild(videoContainer); + widget.appendChild(textContainer); + container.appendChild(widget); + document.getElementById('alertVideoContainer').appendChild(container); + + setTimeout(() => { + container.remove(); + }, delay*1000); + } + + +})(); + diff --git a/modules/alert-video/css/alert-video.css b/modules/alert-video/css/alert-video.css new file mode 100644 index 0000000..1d86de1 --- /dev/null +++ b/modules/alert-video/css/alert-video.css @@ -0,0 +1,133 @@ +#alertVideoContainer { + z-index: 400; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +#alertVideoContainer .alert_outer-container { + display: flex; + justify-content: center; + align-items: center; + width: 800px; + height: 600px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 1rem; + box-sizing: border-box; +} +#alertVideoContainer .alert_outer-container.playing { + animation: 12s alert-animation ease-in-out forwards; +} +#alertVideoContainer .alert_widget-container { + display: flex; + background-color: #FFFFFF00; + width: 100%; + height: 100%; + padding: 16px; + border-radius: 10px; +} +#alertVideoContainer .alert_text-container { + display: flex; + align-items: center; + justify-content: center; + text-align: center; + flex-direction: column; + width: 100%; + position: relative; + z-index: 1; + max-height: 100%; + max-width: 100%; +} +#alertVideoContainer .alert_text { + font-family: 'Roboto', sans-serif; + font-size: 24px; + font-weight: 600; + color: #FFFFFF; + width: 100%; + padding-top: 100px; + animation: hideIn 1.5s, fadeIn 2s 1.5s, fadeOut 2s 8.5s; + animation-fill-mode: forwards; + opacity: 0; + visibility: hidden; +} +#alertVideoContainer .alert_text-accent { + color: #ccaaff; +} +#alertVideoContainer .alert_top-text-container { + position: absolute; + top: 0; + width: 100%; + text-align: center; + padding-top: 1rem; + z-index: 2; + animation: fadeIn 2s 0.5s, fadeOut 2s 10s; + animation-fill-mode: forwards; + opacity: 0; +} +#alertVideoContainer .alert_top-text-container .alert_top-text { + font-family: 'Roboto', sans-serif; + font-size: 24px; + font-weight: 600; + color: #FFFFFF; + width: 100%; + padding-top: 100px; + text-shadow: 0 0 4px #000, 0 0 10px #000; + animation: hideIn 1.5s, fadeIn 2s 1.5s, fadeOut 2s 8.5s; + animation-fill-mode: forwards; + opacity: 0; + visibility: hidden; +} +#alertVideoContainer .alert_bottom-text-container { + position: absolute; + bottom: 0; + width: 100%; + text-align: center; + padding-bottom: 1rem; + z-index: 2; + animation: fadeIn 2s 0.5s, fadeOut 2s 10s; + animation-fill-mode: forwards; + opacity: 0; +} +#alertVideoContainer .alert_bottom-text-container .alert_bottom-text { + font-family: 'Roboto', sans-serif; + font-size: 24px; + font-weight: 600; + color: #FFFFFF; + width: 100%; + padding-top: 100px; + text-shadow: 0 0 4px #000, 0 0 10px #000; + animation: hideIn 1.5s, fadeIn 2s 1.5s, fadeOut 2s 8.5s; + animation-fill-mode: forwards; + opacity: 0; + visibility: hidden; +} +#alertVideoContainer .alertVideoContainer { + display: flex; + width: 50%; + position: absolute; + z-index: -1; + justify-content: center; + align-self: center; +} +#alertVideoContainer .alertVideoContainer .alertVideo { + width: 100%; + height: 100%; +} +@keyframes alert-animation { + 0% { + visibility: hidden; + } + 8.333333333333332% { + visibility: visible; + } + 91.66666666666667% { + opacity: 1; + } + 100% { + opacity: 0; + } +} diff --git a/modules/alert-video/css/alert-video.less b/modules/alert-video/css/alert-video.less new file mode 100644 index 0000000..be24d95 --- /dev/null +++ b/modules/alert-video/css/alert-video.less @@ -0,0 +1,154 @@ +// out: alert-video.css, sourcemap: false, compress: false + +#alertVideoContainer { + z-index: 400; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + .alert_outer-container { + display: flex; + justify-content: center; + align-items: center; + width: 800px; + height: 600px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 1rem; + box-sizing: border-box; + } + .alert_outer-container.playing { + animation: 12s alert-animation ease-in-out forwards; + } + + .alert_widget-container { + display: flex; + background-color: #FFFFFF00; + width: 100%; + height: 100%; + padding: 16px; + border-radius: 10px; + + } + + .alert_text-container { + display: flex; + align-items: center; + justify-content: center; + text-align: center; + flex-direction: column; + width: 100%; + position: relative; + z-index: 1; + max-height: 100%; + max-width: 100%; + } + + .alert_text { + font-family: 'Roboto', sans-serif; + font-size: 24px; + font-weight: 600; + color: #FFFFFF; + width: 100%; + padding-top: 100px; + animation: hideIn 1.5s, fadeIn 2s 1.5s, fadeOut 2s 8.5s; + animation-fill-mode: forwards; + opacity: 0; + visibility: hidden; + } + + .alert_text-accent { + color: #ccaaff; + } + + .alert_top-text-container { + position: absolute; + top: 0; + width: 100%; + text-align: center; + padding-top: 1rem; + z-index: 2; + animation: fadeIn 2s 0.5s, fadeOut 2s 10s; + animation-fill-mode: forwards; + opacity: 0; + + .alert_top-text { + font-family: 'Roboto', sans-serif; + font-size: 24px; + font-weight: 600; + color: #FFFFFF; + width: 100%; + padding-top: 100px; + + text-shadow: 0 0 4px #000, 0 0 10px #000; + + animation: hideIn 1.5s, fadeIn 2s 1.5s, fadeOut 2s 8.5s; + animation-fill-mode: forwards; + opacity: 0; + visibility: hidden; + } + } + + .alert_bottom-text-container { + position: absolute; + bottom: 0; + width: 100%; + text-align: center; + padding-bottom: 1rem; + z-index: 2; + animation: fadeIn 2s 0.5s, fadeOut 2s 10s; + animation-fill-mode: forwards; + opacity: 0; + + .alert_bottom-text { + font-family: 'Roboto', sans-serif; + font-size: 24px; + font-weight: 600; + color: #FFFFFF; + width: 100%; + padding-top: 100px; + + text-shadow: 0 0 4px #000, 0 0 10px #000; + + animation: hideIn 1.5s, fadeIn 2s 1.5s, fadeOut 2s 8.5s; + animation-fill-mode: forwards; + opacity: 0; + visibility: hidden; + + } + } + + .alertVideoContainer { + display: flex; + width: 50%; + position: absolute; + z-index: -1; + justify-content: center; + align-self: center; + + .alertVideo { + width: 100%; + height: 100%; + } + } + + + @keyframes alert-animation { + 0% { + visibility: hidden; + } + 8.333333333333332% { + visibility: visible; + } + 91.66666666666667% { + opacity: 1; + } + 100% { + opacity: 0; + } + } +} \ No newline at end of file diff --git a/modules/alert-video/files/ALERT_cheer.webm b/modules/alert-video/files/ALERT_cheer.webm new file mode 100644 index 0000000..1aa400b Binary files /dev/null and b/modules/alert-video/files/ALERT_cheer.webm differ diff --git a/modules/alert-video/files/ALERT_follow.webm b/modules/alert-video/files/ALERT_follow.webm new file mode 100644 index 0000000..d10c678 Binary files /dev/null and b/modules/alert-video/files/ALERT_follow.webm differ diff --git a/modules/alert-video/files/ALERT_gift.webm b/modules/alert-video/files/ALERT_gift.webm new file mode 100644 index 0000000..71fb6df Binary files /dev/null and b/modules/alert-video/files/ALERT_gift.webm differ diff --git a/modules/alert-video/files/ALERT_raid.webm b/modules/alert-video/files/ALERT_raid.webm new file mode 100644 index 0000000..5c1e008 Binary files /dev/null and b/modules/alert-video/files/ALERT_raid.webm differ diff --git a/modules/alert-video/files/ALERT_sub.webm b/modules/alert-video/files/ALERT_sub.webm new file mode 100644 index 0000000..7db2a0e Binary files /dev/null and b/modules/alert-video/files/ALERT_sub.webm differ diff --git a/modules/alert-video/files/HypeTrainStarted.webm b/modules/alert-video/files/HypeTrainStarted.webm new file mode 100644 index 0000000..8716315 Binary files /dev/null and b/modules/alert-video/files/HypeTrainStarted.webm differ diff --git a/modules/big-emote/big-emote.js b/modules/big-emote/big-emote.js new file mode 100644 index 0000000..ff96aca --- /dev/null +++ b/modules/big-emote/big-emote.js @@ -0,0 +1,125 @@ +;(function() { + const moduleUrl = window.getModuleUrl(); + loadCSSModule('overlay-bigemote-css', moduleUrl + '/css/big-emote.css'); + + function initModule() { + const container = document.getElementById('mainContainer') || document.body; + if (!document.getElementById('bigEmoteContainer')) { + const bigEmoteContainer = document.createElement('div'); + bigEmoteContainer.id = 'bigEmoteContainer'; + container.appendChild(bigEmoteContainer); + } + } + initModule(); + + if (window.SBdispatcher) { + SBdispatcher.on('stream-alert:gigantify_an_emote', data => { + spawnAnimatedImage(data.param1); + }); + } + + function spawnAnimatedImage(url) { + const img = document.createElement("img"); + img.src = url; + img.classList.add("bigEmote", "hidden-scale"); + + img.onload = () => { + const container = document.getElementById("bigEmoteContainer"); + container.appendChild(img); + + img.classList.remove("hidden-scale"); + + // Apparition de l'emote avec un zoom + img.classList.add("appear"); + + // Lancer le déplacement de l'emote après animation initiale + setTimeout(() => { + img.classList.remove("appear"); + + // Démarrer le rebond à partir de la position actuelle + const x = window.innerWidth / 2; + const y = window.innerHeight / 2; + + startBouncing(img, x, y, true); // flag to keep transform translate + }, 3000); + }; + } + + function startBouncing(img, startX, startY) { + const imgSize = 112; + const halfSize = imgSize / 2; + const margin = 50; + + const minX = margin + halfSize; + const maxX = window.innerWidth - margin - halfSize; + const minY = margin + halfSize; + const maxY = window.innerHeight - margin - halfSize; + + let x = startX; + let y = startY; + + const pageDiagonal = Math.sqrt(window.innerWidth ** 2 + window.innerHeight ** 2); + const totalDistance = 3 * pageDiagonal; + const duration = 30000; // 30s + const speed = totalDistance / duration; + + const angle = getValidAngle(); + let dx = Math.cos(angle) * speed; + let dy = Math.sin(angle) * speed; + + let startTime = null; + let fadeOutStarted = false; + + function getValidAngle() { + while (true) { + const angle = Math.random() * 2 * Math.PI; + const deg = angle * (180 / Math.PI); + const prohibited = [0, 90, 180, 270]; + const tooClose = prohibited.some(a => { + const delta = Math.abs(deg - a) % 360; + return delta < 15 || delta > 345; + }); + if (!tooClose) return angle; + } + } + + function animate(timestamp) { + if (!startTime) startTime = timestamp; + const elapsed = timestamp - startTime; + const dt = timestamp - (animate.lastTime || timestamp); + animate.lastTime = timestamp; + + x += dx * dt; + y += dy * dt; + + if (x <= minX || x >= maxX) { + dx = -dx; + x = Math.max(minX, Math.min(x, maxX)); + } + if (y <= minY || y >= maxY) { + dy = -dy; + y = Math.max(minY, Math.min(y, maxY)); + } + + img.style.left = `${x}px`; + img.style.top = `${y}px`; + img.style.transform = "translate(-50%, -50%)"; + + // Déclencher le fade-out à 29s + if (!fadeOutStarted && elapsed >= duration - 1000) { + fadeOutStarted = true; + img.classList.add("fade-out"); + } + + if (elapsed < duration) { + requestAnimationFrame(animate); + } else { + img.remove(); + } + } + + requestAnimationFrame(animate); + } + +})(); + diff --git a/modules/big-emote/css/big-emote.css b/modules/big-emote/css/big-emote.css new file mode 100644 index 0000000..53e47aa --- /dev/null +++ b/modules/big-emote/css/big-emote.css @@ -0,0 +1,47 @@ +#bigEmoteContainer { + z-index: 400; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +#bigEmoteContainer .bigEmote { + position: absolute; + width: 112px; + height: auto; + pointer-events: none; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + transform-origin: center center; +} +#bigEmoteContainer .hidden-scale { + transform: translate(-50%, -50%) scale(0); + opacity: 0; +} +#bigEmoteContainer .appear { + animation: appear-grow-shrink 3s ease-out forwards; +} +#bigEmoteContainer .fade-out { + transition: opacity 1s ease; + opacity: 0; +} +@keyframes appear-grow-shrink { + 0% { + transform: translate(-50%, -50%) scale(0); + opacity: 0; + } + 40% { + transform: translate(-50%, -50%) scale(5); + opacity: 1; + } + 60% { + transform: translate(-50%, -50%) scale(5); + opacity: 1; + } + 100% { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + } +} diff --git a/modules/big-emote/css/big-emote.less b/modules/big-emote/css/big-emote.less new file mode 100644 index 0000000..568d83e --- /dev/null +++ b/modules/big-emote/css/big-emote.less @@ -0,0 +1,54 @@ +// out: big-emote.css, sourcemap: false, compress: false +#bigEmoteContainer { + z-index: 400; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + .bigEmote { + position: absolute; + width: 112px; + height: auto; + pointer-events: none; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + transform-origin: center center; + } + + .hidden-scale { + transform: translate(-50%, -50%) scale(0); + opacity: 0; + } + + .appear { + animation: appear-grow-shrink 3s ease-out forwards; + } + + .fade-out { + transition: opacity 1s ease; + opacity: 0; + } + + @keyframes appear-grow-shrink { + 0% { + transform: translate(-50%, -50%) scale(0); + opacity: 0; + } + 40% { + transform: translate(-50%, -50%) scale(5); + opacity: 1; + } + 60% { + transform: translate(-50%, -50%) scale(5); + opacity: 1; + } + 100% { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + } + } + +} \ No newline at end of file diff --git a/modules/chat/assets/avatar.png b/modules/chat/assets/avatar.png new file mode 100644 index 0000000..21884cb Binary files /dev/null and b/modules/chat/assets/avatar.png differ diff --git a/modules/chat/assets/role-broadcaster.png b/modules/chat/assets/role-broadcaster.png new file mode 100644 index 0000000..6632114 Binary files /dev/null and b/modules/chat/assets/role-broadcaster.png differ diff --git a/modules/chat/assets/role-moderator.png b/modules/chat/assets/role-moderator.png new file mode 100644 index 0000000..607c198 Binary files /dev/null and b/modules/chat/assets/role-moderator.png differ diff --git a/modules/chat/assets/role-vip.png b/modules/chat/assets/role-vip.png new file mode 100644 index 0000000..21fff80 Binary files /dev/null and b/modules/chat/assets/role-vip.png differ diff --git a/modules/chat/chat.js b/modules/chat/chat.js new file mode 100644 index 0000000..48a3cbf --- /dev/null +++ b/modules/chat/chat.js @@ -0,0 +1,455 @@ +////////////////// +// CHAT OVERLAY // +////////////////// + +;(function() { + let sbShowChat = true; + const moduleUrl = window.getModuleUrl(); + loadCSSModule('overlay-chat-css', moduleUrl + '/css/chat.css'); + + function initModule() { + const container = document.body; + + if (!document.getElementById('chat-container')) { + const chatContainer = document.createElement('div'); + chatContainer.className = 'chat-container'; + chatContainer.id = 'chat-container'; + + const sticky = document.createElement('div'); + sticky.id = 'stickyContainer'; + const announceList = document.createElement('ul'); + announceList.id = 'announcementList'; + sticky.appendChild(announceList); + + const messages = document.createElement('div'); + messages.id = 'messagesContainer'; + const messageList = document.createElement('ul'); + messageList.id = 'messageList'; + messages.appendChild(messageList); + + chatContainer.append(sticky, messages); + container.appendChild(chatContainer); + } + + function injectTemplate(id, html) { + if (!document.getElementById(id)) { + const tpl = document.createElement('template'); + tpl.id = id; + tpl.innerHTML = html.trim(); + document.body.appendChild(tpl); + } + } + + injectTemplate('messageTemplate', ` +
+ + + + Avatar + +
+
+ `); + + injectTemplate('announcementTemplate', ` +
+
+
+ `); + } + + initModule(); + + StreamerBot.on('Twitch.ChatMessage', (data) => { + if (sbDebugMode) + console.log(data.data); + TwitchChatMessage(data.data); + }) + + StreamerBot.on('Twitch.Announcement', (data) => { + if (sbDebugMode) + console.log(data.data); + TwitchAnnouncement(data.data); + }) + + StreamerBot.on('Twitch.ChatMessageDeleted', (data) => { + if (sbDebugMode) + console.log(data.data); + TwitchChatMessageDeleted(data.data); + }) + + StreamerBot.on('Twitch.UserBanned', (data) => { + if (sbDebugMode) + console.log(data.data); + TwitchUserBanned(data.data); + }) + + StreamerBot.on('Twitch.UserTimedOut', (data) => { + if (sbDebugMode) + console.log(data.data); + TwitchUserBanned(data.data); + }) + + StreamerBot.on('Twitch.ChatCleared', (data) => { + if (sbDebugMode) + console.log(data.data); + TwitchChatCleared(data.data); + }) + + if (window.SBdispatcher) { + SBdispatcher.on('chat-show', () => { + divShow("chat-container"); + }); + SBdispatcher.on('chat-hide', () => { + divHide("chat-container"); + }); + SBdispatcher.on('chat-toggle', () => { + divToggle("chat-container"); + }); + SBdispatcher.on('chat-left', () => { + divAnimate("chat-container", "chat-container-left"); + }); + SBdispatcher.on('chat-right', () => { + divAnimate("chat-container", "chat-container-right"); + }); + } + + + ////////////////// + // CHAT OPTIONS // + ////////////////// + + const showPlatform = getBooleanParam("showPlatform", true); + const showAvatar = getBooleanParam("showAvatar", true); + const showTimestamps = getBooleanParam("showTimestamps", true); + const showBadges = getBooleanParam("showBadges", true); + const showPronouns = getBooleanParam("showPronouns", true); + const showUsername = getBooleanParam("showUsername", true); + const hideAfter = getIntParam("hideAfter", 900); + const hideAnnounceAfter = getIntParam("hideAfter", 180); + + const ignoreChatters = urlParams.get("ignoreChatters") || "botarex"; + const ignoreUserList = ignoreChatters.split(',').map(item => item.trim().toLowerCase()) || []; + + const chatContainer = document.getElementById('chat-container'); + + if (!sbShowChat) { + chatContainer.style.opacity = 0; + } + + /////////////// + // CHAT CODE // + /////////////// + + function twitchChatDisplay() { + setTimeout(function () { + chatContainer.style.transition = "all 1s ease"; + chatContainer.style.opacity = 1; + }, 1000); + } + + function twitchChatHide() { + setTimeout(function () { + chatContainer.style.transition = "all 1s ease"; + chatContainer.style.opacity = 0; + }, 1000); + } + + async function TwitchChatMessage(data) { + // Don't post messages starting with "!" + if (data.message.message.startsWith("!") && excludeCommands) + return; + + // Don't post messages from users from the ignore list + if (ignoreUserList.includes(data.message.username.toLowerCase())) + return; + + // Get a reference to the template + const template = document.getElementById('messageTemplate'); + + // Create a new instance of the template + const chatMessage = template.content.cloneNode(true); + + // Get divs + const messageContainer = chatMessage.querySelector("#messageContainer"); + const messageDiv = chatMessage.querySelector("#messageContent"); + const usernameDiv = chatMessage.querySelector("#username"); + const timestampDiv = chatMessage.querySelector("#timestamp"); + const badgeListDiv = chatMessage.querySelector("#badges"); + const avatarImg = chatMessage.querySelector("#avatar"); + const roleIconDiv = chatMessage.querySelector("#role"); + + // Set timestamp + if (showTimestamps) { + timestampDiv.innerText = GetCurrentTimeFormatted(); + } + + // Set the username info + if (showUsername) { + usernameDiv.innerText = data.message.displayName; + usernameDiv.style.color = data.message.color; + } + + // Set the message data + let message = data.message.message; + const messageColor = data.message.color; + messageDiv.innerText = message; + + // Set the "action" color + if (data.message.isMe) + messageDiv.style.color = messageColor; + + // Render badges + if (showBadges) { + badgeListDiv.innerHTML = ""; + for (i in data.message.badges) { + const badge = new Image(); + badge.src = data.message.badges[i].imageUrl; + badge.classList.add("badge"); + badgeListDiv.appendChild(badge); + } + } + + // Render emotes + for (i in data.emotes) { + const emoteElement = ``; + const emoteName = EscapeRegExp(data.emotes[i].name); + + let regexPattern = emoteName; + + // Check if the emote name consists only of word characters (alphanumeric and underscore) + if (/^\w+$/.test(emoteName)) { + regexPattern = `\\b${emoteName}\\b`; + } + else { + // For non-word emotes, ensure they are surrounded by non-word characters or boundaries + regexPattern = `(?:^|[^\\w])${emoteName}(?:$|[^\\w])`; + } + + const regex = new RegExp(regexPattern, 'g'); + messageDiv.innerHTML = messageDiv.innerHTML.replace(regex, emoteElement); + } + + // Render cheermotes + for (i in data.cheerEmotes) { + const bits = data.cheerEmotes[i].bits; + const imageUrl = data.cheerEmotes[i].imageUrl; + const name = data.cheerEmotes[i].name; + const cheerEmoteElement = ``; + const bitsElements = `${bits}` + messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements); + } + + // Render avatars + if (showAvatar) { + const username = data.message.username; + const avatarURL = await GetAvatar(username); + avatarImg.src = avatarURL; + } + + // Add User Role Style + switch (data.user.role) { + case 2: + // User VIP + messageContainer.classList.add("vip"); + const roleVip = new Image(); + roleVip.src = moduleUrl + "/assets/role-vip.png"; + roleIconDiv.appendChild(roleVip); + break; + case 3: + // User Moderator + messageContainer.classList.add("mod"); + const roleMod = new Image(); + roleMod.src = moduleUrl + "/assets/role-moderator.png"; + roleIconDiv.appendChild(roleMod); + break; + case 4: + // User Broadcaster + messageContainer.classList.add("streamer"); + const roleStreamer = new Image(); + roleStreamer.src = moduleUrl + "/assets/role-broadcaster.png"; + roleIconDiv.appendChild(roleStreamer); + break; + default: + messageContainer.classList.add("user"); + } + + addMessage(chatMessage,data.message.msgId, data.user.id); + } + + function TwitchChatMessageDeleted(data) { + const messageList = document.getElementById("messageList"); + + // Maintain a list of chat messages to delete + const messagesToRemove = []; + + // ID of the message to remove + const messageId = data.messageId; + + // Find the items to remove + for (let i = 0; i < messageList.children.length; i++) { + if (messageList.children[i].id === messageId) { + messagesToRemove.push(messageList.children[i]); + } + } + + // Remove the items + messagesToRemove.forEach(item => { + item.style.opacity = 0; + item.style.height = 0; + setTimeout(function () { + messageList.removeChild(item); + }, 500); + }); + } + + function TwitchUserBanned(data) { + const messageList = document.getElementById("messageList"); + + // Maintain a list of chat messages to delete + const messagesToRemove = []; + + // ID of the message to remove + const userId = data.user_id; + + // Find the items to remove + for (let i = 0; i < messageList.children.length; i++) { + if (messageList.children[i].dataset.userId === userId) { + messagesToRemove.push(messageList.children[i]); + } + } + + // Remove the items + messagesToRemove.forEach(item => { + messageList.removeChild(item); + }); + } + + function TwitchChatCleared(data) { + const messageList = document.getElementById("messageList"); + + while (messageList.firstChild) { + messageList.removeChild(messageList.firstChild); + } + } + + async function TwitchAnnouncement(data) { + // Get a reference to the template + const template = document.getElementById('announcementTemplate'); + + // Create a new instance of the template + const stickyMessage = template.content.cloneNode(true); + const messageDiv = stickyMessage.querySelector("#messageContent"); + + // Set the message data + let message = data.text; + messageDiv.innerText = message; + + for (i in data.parts) { + if (data.parts[i].type == `emote`) { + const emoteElement = ``; + const emoteName = EscapeRegExp(data.parts[i].text); + + let regexPattern = emoteName; + + // Check if the emote name consists only of word characters (alphanumeric and underscore) + if (/^\w+$/.test(emoteName)) { + regexPattern = `\\b${emoteName}\\b`; + } + else { + // For non-word emotes, ensure they are surrounded by non-word characters or boundaries + regexPattern = `(?:^|[^\\w])${emoteName}(?:$|[^\\w])`; + } + + const regex = new RegExp(regexPattern, 'g'); + messageDiv.innerHTML = messageDiv.innerHTML.replace(regex, emoteElement); + } + } + + + addAnnounce(stickyMessage, data.messageId, data.user.id); + + } + + function addMessage(messageElement,elementId,userId) { + const chatMessages = document.getElementById("messageList"); + var lineItem = document.createElement('li'); + lineItem.id = elementId; + lineItem.dataset.userId = userId; + lineItem.appendChild(messageElement); + + chatMessages.appendChild(lineItem); + chatMessages.scrollTop = chatMessages.scrollHeight; + + if (hideAfter > 0) { + setTimeout(function () { + lineItem.style.opacity = 0; + setTimeout(function () { + chatMessages.removeChild(lineItem); + }, 500); + }, hideAfter * 1000); + } + + } + + function addAnnounce(messageElement,elementId,userId) { + const chatMessages = document.getElementById("announcementList"); + // Clear the previous announcements + while (chatMessages.firstChild) { + chatMessages.removeChild(chatMessages.firstChild); + } + + var lineItem = document.createElement('li'); + lineItem.id = elementId; + lineItem.dataset.userId = userId; + lineItem.appendChild(messageElement); + + chatMessages.appendChild(lineItem); + chatMessages.scrollTop = chatMessages.scrollHeight; + + if (hideAnnounceAfter > 0) { + setTimeout(function () { + lineItem.style.opacity = 0; + setTimeout(function () { + chatMessages.removeChild(lineItem); + }, 500); + }, hideAnnounceAfter * 1000); + } + + } + + function GetCurrentTimeFormatted() { + const now = new Date(); + let hours = now.getHours(); + const minutes = String(now.getMinutes()).padStart(2, '0'); + + const formattedTime = `${hours}:${minutes}`; + return formattedTime; + + // const ampm = hours >= 12 ? 'PM' : 'AM'; + + // hours = hours % 12; + // hours = hours ? hours : 12; // the hour '0' should be '12' + + // const formattedTime = `${hours}:${minutes} ${ampm}`; + // return formattedTime; + } + + async function GetAvatar(username) { + if (avatarMap.has(username)) { + console.debug(`Avatar found for ${username}. Retrieving from hash map.`) + return avatarMap.get(username); + } + else { + console.debug(`No avatar found for ${username}. Retrieving from Decapi.`) + let response = await fetch('https://decapi.me/twitch/avatar/' + username); + let data = await response.text() + avatarMap.set(username, data); + return data; + } + } + + function EscapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + } +})(); diff --git a/modules/chat/css/chat.css b/modules/chat/css/chat.css new file mode 100644 index 0000000..081b747 --- /dev/null +++ b/modules/chat/css/chat.css @@ -0,0 +1,167 @@ +#chat-container { + z-index: 200; + transition: opacity 1s ease; + position: absolute; + top: 20px; + left: 20px; + width: 700px; + height: 750px; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0); +} +#chat-container #stickyContainer { + width: 100%; + position: sticky; + top: 0; + z-index: 50; + flex-shrink: 0; + text-align: center; +} +#chat-container #messagesContainer { + width: 100%; + height: 100%; + z-index: 50; + display: flex; + flex-direction: column; + justify-content: flex-end; + overflow-y: auto; + padding: 10px; +} +#chat-container ul { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + overflow-y: auto; + display: flex; + flex-direction: column; + justify-content: flex-end; +} +#chat-container li { + list-style: none; + margin: 0px; + padding: 0px; +} +#chat-container .chat-message { + position: relative; + background-color: rgba(51, 51, 51, 0.75); + border-radius: 12px; + padding: 16px; + margin: 10px 42px 40px 24px; + color: #f0f0f0; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4); +} +#chat-container .chat-message.mod { + border-left: 4px solid #4aa3ff; +} +#chat-container .chat-message.streamer { + border-left: 4px solid #ff5f5f; +} +#chat-container .label { + position: absolute; + padding: 4px; + font-size: 0.8rem; + background: none; + border-radius: 6px; + box-shadow: none; + white-space: nowrap; +} +#chat-container .label.username { + top: 0; + left: 24px; + transform: translateY(-60%); + font-size: 1.3rem; + font-weight: bold; + color: #a0d2ff; + text-shadow: 1px 1px 5px rgba(255, 255, 255, 0.75); +} +#chat-container .label.top-timestamp { + top: 0; + left: 50%; + transform: translate(-50%, -50%); + font-style: italic; + opacity: 0.85; +} +#chat-container .label.bottom-timestamp { + top: 100%; + left: 50%; + transform: translate(-50%, -50%); + position: absolute; + font-style: italic; + opacity: 0.85; +} +#chat-container .label.badges { + top: 0; + right: 48px; + transform: translateY(-50%); + display: flex; + justify-content: flex-end; + align-items: center; + gap: 4px; +} +#chat-container .label.badges img { + width: 24px; + height: 24px; + vertical-align: middle; +} +#chat-container .avatar { + position: absolute; + top: 0; + right: 0; + width: 48px; + height: 48px; + transform: translate(50%, -50%); + border-radius: 50%; + object-fit: cover; + border: 2px solid #444; +} +#chat-container .role-icon { + position: absolute; + top: 0; + left: 0; + transform: translate(-50%, -50%); + font-size: 1.4rem; + opacity: 0.95; +} +#chat-container .role-icon img { + width: 48px; + height: 48px; + vertical-align: middle; +} +#chat-container .chat-message.message-content { + font-size: 1rem; + line-height: 1.4; + word-wrap: break-word; +} +#chat-container .announcement-message { + position: relative; + background-color: rgba(51, 51, 51, 0.75); + border-radius: 12px; + padding: 16px; + text-align: center; + font-weight: bold; + font-size: 1.1rem; + margin: 5px 5px 10px 5px; + color: #f0f0f0; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4); +} +#chat-container .announcement-message.message-content { + font-size: 1.2rem; + word-wrap: break-word; +} +#chat-container .emote { + height: 1.5em; + margin: 1px; + transform: translate(0px, 0.4em); +} +@keyframes chat-container-left { + to { + transform: translateX(0); + } +} +@keyframes chat-container-right { + to { + transform: translateX(calc(100vw - 40px - 100%)); + } +} diff --git a/modules/chat/css/chat.less b/modules/chat/css/chat.less new file mode 100644 index 0000000..6043432 --- /dev/null +++ b/modules/chat/css/chat.less @@ -0,0 +1,188 @@ +// out: chat.css, sourcemap: false, compress: false + +#chat-container { + z-index: 200; + transition: opacity 1s ease; + position: absolute; + top: 20px; + left: 20px; + width: 700px; + height: 750px; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0); + + #stickyContainer { + width: 100%; + position: sticky; + top: 0; + z-index: 50; + text-align: center; + flex-shrink: 0; + text-align: center; + } + + #messagesContainer { + width: 100%; + height: 100%; + z-index: 50; + display: flex; + flex-direction: column; + justify-content: flex-end; + overflow-y: auto; + padding: 10px; + } + + ul { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + overflow-y: auto; + display: flex; + flex-direction: column; + justify-content: flex-end; + } + + li { + list-style: none; + margin: 0px; + padding: 0px; + } + + .chat-message { + position: relative; + background-color: rgba(51, 51, 51, 0.75); + border-radius: 12px; + padding: 16px; + margin: 10px 42px 40px 24px; + color: #f0f0f0; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4); + } + + .chat-message.mod { + border-left: 4px solid #4aa3ff; + } + + .chat-message.streamer { + border-left: 4px solid #ff5f5f; + } + + .label { + position: absolute; + padding: 4px; + font-size: 0.80rem; + background: none; + border-radius: 6px; + box-shadow: none; + white-space: nowrap; + } + + .label.username { + top: 0; + left: 24px; + transform: translateY(-60%); + font-size: 1.30rem; + font-weight: bold; + color: #a0d2ff; + text-shadow: 1px 1px 5px rgba(255, 255, 255, 0.75); + } + + .label.top-timestamp { + top: 0; + left: 50%; + transform: translate(-50%, -50%); + font-style: italic; + opacity: 0.85; + } + + .label.bottom-timestamp { + top: 100%; + left: 50%; + transform: translate(-50%, -50%); + position: absolute; + font-style: italic; + opacity: 0.85; + } + + .label.badges { + top: 0; + right: 48px; + transform: translateY(-50%); + display: flex; + justify-content: flex-end; + align-items: center; + gap: 4px; + } + + .label.badges img { + width: 24px; + height: 24px; + vertical-align: middle; + } + + .avatar { + position: absolute; + top: 0; + right: 0; + width: 48px; + height: 48px; + transform: translate(50%, -50%); + border-radius: 50%; + object-fit: cover; + border: 2px solid #444; + } + + .role-icon { + position: absolute; + top: 0; + left: 0; + transform: translate(-50%, -50%); + font-size: 1.4rem; + opacity: 0.95; + } + + .role-icon img { + width: 48px; + height: 48px; + vertical-align: middle; + } + + .chat-message.message-content { + font-size: 1rem; + line-height: 1.4; + word-wrap: break-word; + } + + .announcement-message { + position: relative; + background-color: rgba(51, 51, 51, 0.75); + border-radius: 12px; + padding: 16px; + text-align: center; + font-weight: bold; + font-size: 1.1rem; + margin: 5px 5px 10px 5px; + color: #f0f0f0; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4); + } + + .announcement-message.message-content { + font-size: 1.2rem; + word-wrap: break-word; + } + + .emote { + height: 1.5em; + margin: 1px; + transform: translate(0px, 0.4em); + } +} + +@keyframes chat-container-left { + to { transform: translateX(0); } +} + +@keyframes chat-container-right { + to { transform: translateX(calc(100vw - 40px - 100%)); } +} \ No newline at end of file diff --git a/modules/popup/css/popup.css b/modules/popup/css/popup.css new file mode 100644 index 0000000..22cc8de --- /dev/null +++ b/modules/popup/css/popup.css @@ -0,0 +1,24 @@ +#popupContainer { + z-index: 400; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +#popupContainer .overlay-popup { + position: absolute; + transform: translate(-50%, -50%); + z-index: 9999; + padding: 1em 1.5em; + background: rgba(0, 0, 0, 0.75); + color: #fff; + border-radius: 0.5em; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + max-width: 20%; + text-align: center; + transition: opacity 0.5s ease-out; +} +#popupContainer .overlay-popup.fade-out { + opacity: 0; +} diff --git a/modules/popup/css/popup.less b/modules/popup/css/popup.less new file mode 100644 index 0000000..af5133f --- /dev/null +++ b/modules/popup/css/popup.less @@ -0,0 +1,31 @@ +// out: popup.css, sourcemap: false, compress: false + +#popupContainer { + z-index: 400; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + .overlay-popup { + position: absolute; + transform: translate(-50%, -50%); + z-index: 9999; + padding: 1em 1.5em; + background: rgba(0,0,0,0.75); + color: #fff; + border-radius: 0.5em; + box-shadow: 0 0 10px rgba(0,0,0,0.5); + max-width: 20%; + text-align: center; + transition: opacity 0.5s ease-out; + } + + .overlay-popup.fade-out { + opacity: 0; + } + +} + + diff --git a/modules/popup/popup.js b/modules/popup/popup.js new file mode 100644 index 0000000..d3f6fcb --- /dev/null +++ b/modules/popup/popup.js @@ -0,0 +1,98 @@ +;(function() { + const moduleUrl = window.getModuleUrl(); + loadCSSModule('overlay-popup-css', moduleUrl + '/css/popup.css'); + + function initModule() { + const container = document.getElementById('mainContainer') || document.body; + if (!document.getElementById('popupContainer')) { + const popupContainer = document.createElement('div'); + popupContainer.id = 'popupContainer'; + container.appendChild(popupContainer); + } + } + initModule(); + + if (window.SBdispatcher) { + SBdispatcher.on('stream-popup', data => { + showPopup( + data.param1, + data.param2, + parseFloat(data.param3) || 3, + data.param4 != null ? parseFloat(data.param4) : -1, + data.param5 != null ? parseFloat(data.param5) : -1 + ); + }); + } + + function randomPercent(min = 0, max = 100) { + const minClamped = Math.ceil(min); + const maxClamped = Math.floor(max); + return Math.floor(Math.random() * (maxClamped - minClamped + 1)) + minClamped; + } + + function isOverlapping(r1, r2) { + return !(r1.right < r2.left || r1.left > r2.right || r1.bottom < r2.top || r1.top > r2.bottom); + } + + // Tableau global des popups actifs pour éviter le chevauchement + window._activePopups = window._activePopups || []; + + /** + * Affiche un popup dans #mainContainer (ou body si absent). + * @param {string} title Titre du popup + * @param {string} message Contenu HTML du message + * @param {number} delaySeconds Durée d'affichage en secondes + * @param {number|null} posX Position horizontale en %, -1 ou null = aléatoire + * @param {number|null} posY Position verticale en %, -1 ou null = aléatoire + */ + function showPopup(title, message, delaySeconds, posX = -1, posY = -1) { + const container = document.getElementById('popupContainer') || document.body; + const activePopups = window._activePopups; + const maxAttempts = 10; + let attempt = 0; + let popup, rect; + + do { + if (popup) { + popup.remove(); + } + // Calcul position (entre 10% et 90% si aléatoire) + const x = (posX == null || posX < 0) ? randomPercent(10, 90) : posX; + const y = (posY == null || posY < 0) ? randomPercent(10, 90) : posY; + + // Création de l'élément popup + popup = document.createElement('div'); + popup.classList.add('overlay-popup'); + popup.style.position = 'absolute'; + popup.style.left = `${x}%`; + popup.style.top = `${y}%`; + + // Contenu + const header = document.createElement('h3'); + header.innerText = title; + const body = document.createElement('div'); + body.innerHTML = message; + popup.append(header, body); + + container.appendChild(popup); + rect = popup.getBoundingClientRect(); + attempt++; + } while ( + attempt < maxAttempts && + activePopups.some(other => isOverlapping(rect, other.getBoundingClientRect())) + ); + + // Conserve ce popup dans la liste des actifs + activePopups.push(popup); + + // Suppression après délai et retrait de la liste + setTimeout(() => { + popup.classList.add('fade-out'); + setTimeout(() => { + popup.remove(); + const idx = activePopups.indexOf(popup); + if (idx !== -1) activePopups.splice(idx, 1); + }, 500); + }, delaySeconds * 1000); + } +})(); diff --git a/modules/scrolling-banner/css/fonts/led_counter-7.ttf b/modules/scrolling-banner/css/fonts/led_counter-7.ttf new file mode 100644 index 0000000..6ce510f Binary files /dev/null and b/modules/scrolling-banner/css/fonts/led_counter-7.ttf differ diff --git a/modules/scrolling-banner/css/fonts/led_counter-7_italic.ttf b/modules/scrolling-banner/css/fonts/led_counter-7_italic.ttf new file mode 100644 index 0000000..fcbd26b Binary files /dev/null and b/modules/scrolling-banner/css/fonts/led_counter-7_italic.ttf differ diff --git a/modules/scrolling-banner/css/fonts/led_counter-7_screen.gif b/modules/scrolling-banner/css/fonts/led_counter-7_screen.gif new file mode 100644 index 0000000..3097516 Binary files /dev/null and b/modules/scrolling-banner/css/fonts/led_counter-7_screen.gif differ diff --git a/modules/scrolling-banner/css/scrolling-banner.css b/modules/scrolling-banner/css/scrolling-banner.css new file mode 100644 index 0000000..6f6eef0 --- /dev/null +++ b/modules/scrolling-banner/css/scrolling-banner.css @@ -0,0 +1,35 @@ +@font-face { + font-family: 'led_counter-7'; + src: url('fonts/led_counter-7.ttf') format('truetype'); +} +#scrollingBannerContainer { + z-index: 500; + position: fixed; + top: 30px; + left: -100%; + width: 70%; + height: 75px; + background: linear-gradient(to right, black, #800000); + display: flex; + align-items: center; + overflow: hidden; + clip-path: polygon(0 0, 100% 0, calc(100% - 30px) 100%, 0 100%); +} +#scrollingBannerContainer #scrollingBannerMessage { + width: 100%; + color: limegreen; + font-family: 'led_counter-7', monospace; + font-size: 64px; + white-space: nowrap; + display: inline-block; + position: absolute; + animation: scrolling 10s linear infinite; +} +@keyframes scrolling { + 0% { + transform: translateX(100%); + } + 100% { + transform: translateX(-100%); + } +} diff --git a/modules/scrolling-banner/css/scrolling-banner.less b/modules/scrolling-banner/css/scrolling-banner.less new file mode 100644 index 0000000..af57162 --- /dev/null +++ b/modules/scrolling-banner/css/scrolling-banner.less @@ -0,0 +1,40 @@ +// out: scrolling-banner.css, sourcemap: false, compress: false + +@font-face { + font-family: 'led_counter-7'; + src: url('fonts/led_counter-7.ttf') format('truetype'); +} + +#scrollingBannerContainer { + z-index: 500; + position: fixed; + top: 30px; + left: -100%; + width: 70%; + height: 75px; + background: linear-gradient(to right, black, #800000); + display: flex; + align-items: center; + overflow: hidden; + clip-path: polygon( 0 0, 100% 0, calc(100% - 30px) 100%, 0 100% ); + + #scrollingBannerMessage { + width: 100%; + color: limegreen; + font-family: 'led_counter-7', monospace; + font-size: 64px; + white-space: nowrap; + display: inline-block; + position: absolute; + animation: scrolling 10s linear infinite; + } + + @keyframes scrolling { + 0% { + transform: translateX(100%); + } + 100% { + transform: translateX(-100%); + } + } +} diff --git a/modules/scrolling-banner/scrolling-banner.js b/modules/scrolling-banner/scrolling-banner.js new file mode 100644 index 0000000..1693fd7 --- /dev/null +++ b/modules/scrolling-banner/scrolling-banner.js @@ -0,0 +1,50 @@ +;(function() { + const moduleUrl = window.getModuleUrl(); + loadCSSModule('overlay-scrollingbanner-css', moduleUrl + '/css/scrolling-banner.css'); + + function initModule() { + const container = document.getElementById('mainContainer') || document.body; + if (!document.getElementById('scrollingBannerContainer')) { + const scrollingBannerContainer = document.createElement('div'); + scrollingBannerContainer.id = 'scrollingBannerContainer'; + + const scrollingBannerMessage = document.createElement('div'); + scrollingBannerMessage.id = 'scrollingBannerMessage'; + scrollingBannerMessage.innerText = 'Bienvenue sur le stream !' + + scrollingBannerContainer.appendChild(scrollingBannerMessage); + container.appendChild(scrollingBannerContainer); + } + } + initModule(); + + if (window.SBdispatcher) { + SBdispatcher.on('stream-scrollingbanner', data => { + showAnnounce(data.message); + }); + SBdispatcher.on('stream-scrollingbanner-hide', () => { + hideAnnounce(); + }); + } + + function showAnnounce(message = "") { + const container = document.getElementById('scrollingBannerContainer'); + const announceText = document.getElementById('scrollingBannerMessage'); + + if (message.length > 0) { + announceText.innerText = message; + } + container._positionDivHandler = () => positionDiv(container); + container.addEventListener('animationend', container._positionDivHandler); + container.style.animation = "slide-in-left 2s ease forwards"; + } + + function hideAnnounce() { + const container = document.getElementById('scrollingBannerContainer'); + container._positionDivHandler = () => positionDiv(container); + container.addEventListener('animationend', container._positionDivHandler); + container.style.animation = "slide-out-left 2s ease forwards"; + } + +})(); + diff --git a/overlay-core.js b/overlay-core.js new file mode 100644 index 0000000..18a17e5 --- /dev/null +++ b/overlay-core.js @@ -0,0 +1,248 @@ +//////////////// +// PARAMETERS // +//////////////// +const queryString = window.location.search; +const urlParams = new URLSearchParams(queryString); + +const sbDebugMode = urlParams.get("debug") || false; +const sbServerAddress = urlParams.get("address") || "127.0.0.1"; +const sbServerPort = urlParams.get("port") || "8080"; +const sbServerPassword = urlParams.get("password") || "VerySecretPassword"; + +const avatarMap = new Map(); + +///////////////////////// +// STREAMER.BOT CLIENT // +///////////////////////// + +const StreamerBot = new StreamerbotClient({ + host: sbServerAddress, + port: sbServerPort, + password: sbServerPassword, + + onConnect: (data) => { + console.log(`Streamer.bot successfully connected to ${sbServerAddress}:${sbServerPort}`) + if (sbDebugMode) + console.debug(data); + SetConnectionStatus(true); + }, + + onDisconnect: () => { + console.error(`Streamer.bot disconnected from ${sbServerAddress}:${sbServerPort}`) + SetConnectionStatus(false); + } + +}); + +///////////////////////// +// STREAMER.BOT STATUS // +///////////////////////// + +function SetConnectionStatus(connected) { + let statusContainer = document.getElementById("statusContainer"); + if (connected) { + statusContainer.style.background = "#2FB774"; + statusContainer.innerText = "Connected!"; + statusContainer.style.opacity = 1; + setTimeout(() => { + statusContainer.style.transition = "all 2s ease"; + statusContainer.style.opacity = 0; + }, 10); + } + else { + statusContainer.style.background = "#D12025"; + statusContainer.innerText = "Connecting..."; + statusContainer.style.transition = ""; + statusContainer.style.opacity = 1; + } +} + +///////////////////////////// +// STREAMER.BOT DISPATCHER // +///////////////////////////// +const SBdispatcher = { + _handlers: {}, + on(eventName, handler) { + if (!this._handlers[eventName]) this._handlers[eventName] = [] + this._handlers[eventName].push(handler) + }, + emit(eventName, data) { + (this._handlers[eventName] || []).forEach(h => { + try { h(data) } + catch(e) { console.error(`Error in handler for ${eventName}:`, e) } + }) + } +} + +window.SBdispatcher = SBdispatcher + +/////////////////////////////// +// STREAMER.BOT SUBSCRIPTION // +/////////////////////////////// +StreamerBot.on('General.Custom', (data) => { + const d = data.data + if (sbDebugMode) console.log(d) + if (d.origin !== "antarex-overlay") return + SBdispatcher.emit(d.action, d) + if (d.type) { + SBdispatcher.emit(`${d.action}:${d.type}`, d) + } +}); + +/////////////////////// +// OVERLAY FUNCTIONS // +/////////////////////// +function loadCSSModule(id, href) { + if (document.getElementById(id)) return; + const link = document.createElement('link'); + link.id = id; + link.rel = 'stylesheet'; + link.href = href; + document.head.appendChild(link); +} + +function getModuleUrl() { + const thisScript = document.currentScript; + if (!thisScript) { + console.warn('getModuleUrl: document.currentScript non disponible'); + return ''; + } + const url = thisScript.src; + return url.substring(0, url.lastIndexOf('/')); +} + +function getBooleanParam(paramName, defaultValue) { + const urlParams = new URLSearchParams(window.location.search); + const paramValue = urlParams.get(paramName); + + if (paramValue === null) { + return defaultValue; // Parameter not found + } + + const lowercaseValue = paramValue.toLowerCase(); // Handle case-insensitivity + + if (lowercaseValue === 'true') { + return true; + } else if (lowercaseValue === 'false') { + return false; + } else { + return paramValue; // Return original string if not 'true' or 'false' + } +} + +function getIntParam(paramName, defaultValue) { + const urlParams = new URLSearchParams(window.location.search); + const paramValue = urlParams.get(paramName); + + if (paramValue === null) { + return defaultValue; // or undefined, or a default value, depending on your needs + } + + console.log(paramValue); + + const intValue = parseInt(paramValue, 10); // Parse as base 10 integer + + if (isNaN(intValue)) { + return null; // or handle the error in another way, e.g., throw an error + } + + return intValue; +} + +function divShow(divId) { + const div = document.getElementById(divId); + if (div) { + div.style.transition = "all 1s ease"; + div.style.opacity = 1; + } else { + console.error(`Element with ID ${divId} not found.`); + } +} + +function divHide(divId) { + const div = document.getElementById(divId); + if (div) { + div.style.transition = "all 1s ease"; + div.style.opacity = 0; + } else { + console.error(`Element with ID ${divId} not found.`); + } +} + +function divToggle(divId) { + const div = document.getElementById(divId); + if (div) { + div.style.transition = "all 1s ease"; + + const currentOpacity = window.getComputedStyle(div).opacity; + + if (parseFloat(currentOpacity) === 0) { + div.style.opacity = 1; + } else { + div.style.opacity = 0; + } + } else { + console.error(`Element with ID ${divId} not found.`); + } +} + +function divAnimate(divId, animationName) { + const div = document.getElementById(divId); + if (div) { + function positionDiv() { + finalizeTransformPosition(div); + div.removeEventListener('animationend', positionDiv); + } + div.addEventListener('animationend', positionDiv); + div.style.animation = `${animationName} 2s ease forwards`; + } else { + console.error(`Element with ID ${divId} not found.`); + } +} + +function finalizePosition(frame) { + const rect = frame.getBoundingClientRect(); + const parentRect = frame.offsetParent + ? frame.offsetParent.getBoundingClientRect() + : { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight }; + + const fromLeft = rect.left - parentRect.left; + const fromRight = parentRect.right - rect.right; + const fromTop = rect.top - parentRect.top; + const fromBottom = parentRect.bottom - rect.bottom; + + if (fromRight <= fromLeft) { + // Closer to right edge → use right + frame.style.right = `${fromRight}px`; + frame.style.left = ''; + } else { + frame.style.left = `${fromLeft}px`; + frame.style.right = ''; + } + + if (fromBottom <= fromTop) { + frame.style.bottom = `${fromBottom}px`; + frame.style.top = ''; + } else { + frame.style.top = `${fromTop}px`; + frame.style.bottom = ''; + } + + frame.style.animation = ""; +} + +function finalizeTransformPosition(div) { + const computedStyle = window.getComputedStyle(div); + const transform = computedStyle.transform; + div.style.transform = transform; + div.style.animation = ""; +} + +function positionDiv(container) { + finalizePosition(container); + container.removeEventListener('animationend', container._positionDivHandler); +} + +window.loadCSSModule = loadCSSModule; +window.getModuleUrl = getModuleUrl; +window.getBooleanParam = getBooleanParam; diff --git a/overlay.css b/overlay.css new file mode 100644 index 0000000..2676337 --- /dev/null +++ b/overlay.css @@ -0,0 +1,69 @@ +body, +html { + margin: 0; + padding: 0; + overflow: hidden; + width: 100%; + height: 100%; + background: transparent; + position: relative; +} +#statusContainer { + z-index: 100; + font-weight: 500; + font-size: 12px; + text-align: center; + background-color: #D12025; + color: white; + padding: 5px 20px; + border-radius: 7px; + position: absolute; + bottom: 10px; + right: 10px; +} +#mainContainer { + padding: 0; + margin: 0; +} + +@keyframes slide-in-left { + to { left: 0; } +} + +@keyframes slide-out-left { + to { left: -100%; } +} + +@keyframes slide-in-right { + to { + right: 0; + } +} + +@keyframes slide-out-right { + to { + right: -100%; + } +} + +@keyframes hideIn { + 99% { + visibility: hidden; + } + 100% { + visibility: visible; + } +} +@keyframes fadeIn { + 100% { + opacity: 1; + } +} +@keyframes fadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} diff --git a/overlay.html b/overlay.html new file mode 100644 index 0000000..61ec306 --- /dev/null +++ b/overlay.html @@ -0,0 +1,20 @@ + + + + Antarex | Streamer.bot Overlay + + + + + +
Connecting...
+
+ + + + + + + + + \ No newline at end of file diff --git a/websocket-streamerbot.cs b/websocket-streamerbot.cs new file mode 100644 index 0000000..d180943 --- /dev/null +++ b/websocket-streamerbot.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +public class CPHInline +{ + public bool Execute() + { + Dictionary message = new Dictionary(); + Dictionary content = new Dictionary(); + + message.Add("origin",args.ContainsKey("wsOrigin") ? args["wsOrigin"].ToString() : "antarex-overlay"); + message.Add("action",args.ContainsKey("wsAction") ? args["wsAction"].ToString() : "none"); + + if (args.ContainsKey("wsType")) message.Add("type",args["wsType"].ToString()); + if (args.ContainsKey("user")) message.Add("user",args["user"].ToString()); + if (args.ContainsKey("userName")) message.Add("username",args["userName"].ToString()); + if (args.ContainsKey("userId")) message.Add("userid",args["userId"].ToString()); + if (args.ContainsKey("message")) message.Add("message",args["message"].ToString()); + + if (args.ContainsKey("wsParam1")) message.Add("param1",args["wsParam1"]); + if (args.ContainsKey("wsParam2")) message.Add("param2",args["wsParam2"]); + if (args.ContainsKey("wsParam3")) message.Add("param3",args["wsParam3"]); + if (args.ContainsKey("wsParam4")) message.Add("param4",args["wsParam4"]); + if (args.ContainsKey("wsParam5")) message.Add("param5",args["wsParam5"]); + + var jsonMessage = JsonConvert.SerializeObject(message); + CPH.WebsocketBroadcastJson(jsonMessage); + return true; + } +}