This commit is contained in:
Stephane Bouvard
2025-07-23 14:47:19 +02:00
commit d7101033e7
36 changed files with 2250 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@

View File

@@ -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 = '<p></p>';
const alertBannerMessage = document.createElement('div');
alertBannerMessage.id = 'alertBannerMessage';
alertBannerMessage.innerHTML = '<p></p>';
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";
}
})();

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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 &agrave; <span class="alert_text-accent alert_variable-username">' + data.param1 + '</span>',
// bottomLine: 'Il a déjà offert <span class="alert_text-accent alert_variable-username">' + data.param2 +'</span> 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 dutilisateur (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);
}
})();

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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);
}
})();

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

455
modules/chat/chat.js Normal file
View File

@@ -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', `
<div class="chat-message" id="messageContainer">
<span class="label username" id="username"></span>
<span class="label bottom-timestamp" id="timestamp"></span>
<span class="label badges" id="badges"></span>
<img class="avatar" id="avatar" alt="Avatar" />
<span class="role-icon" id="role"></span>
<div class="message-content" id="messageContent"></div>
</div>
`);
injectTemplate('announcementTemplate', `
<div class="announcement-message" id="messageContainer">
<div class="message-content" id="messageContent"></div>
</div>
`);
}
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 = `<img src="${data.emotes[i].imageUrl}" class="emote"/>`;
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 = `<img src="${imageUrl}" class="emote"/>`;
const bitsElements = `<span class="bits">${bits}</span>`
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 = `<img src="${data.parts[i].imageUrl}" class="emote"/>`;
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
}
})();

167
modules/chat/css/chat.css Normal file
View File

@@ -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%));
}
}

188
modules/chat/css/chat.less Normal file
View File

@@ -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%)); }
}

View File

@@ -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;
}

View File

@@ -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;
}
}

98
modules/popup/popup.js Normal file
View File

@@ -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);
}
})();

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -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%);
}
}

View File

@@ -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%);
}
}
}

View File

@@ -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";
}
})();

248
overlay-core.js Normal file
View File

@@ -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;

69
overlay.css Normal file
View File

@@ -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;
}
}

20
overlay.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>Antarex | Streamer.bot Overlay</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="overlay.css">
<script type="text/javascript" src="https://unpkg.com/@streamerbot/client/dist/streamerbot-client.js"></script>
</head>
<body>
<div id="statusContainer">Connecting...</div>
<div id="mainContainer"></div>
</body>
<script type="text/javascript" src="overlay-core.js"></script>
<script type="text/javascript" src="modules/popup/popup.js"></script>
<script type="text/javascript" src="modules/chat/chat.js"></script>
<script type="text/javascript" src="modules/scrolling-banner/scrolling-banner.js"></script>
<script type="text/javascript" src="modules/alert-banner/alert-banner.js"></script>
<script type="text/javascript" src="modules/big-emote/big-emote.js"></script>
<script type="text/javascript" src="modules/alert-video/alert-video.js"></script>
</html>

31
websocket-streamerbot.cs Normal file
View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
public class CPHInline
{
public bool Execute()
{
Dictionary<string, object> message = new Dictionary<string, object>();
Dictionary<string, object> content = new Dictionary<string, object>();
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;
}
}