mirror of
https://github.com/nuttylmao/nutty.gg.git
synced 2026-09-19 04:00:59 -04:00
Ported 'Twitch Ad Countdown Timer'
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="shortcut icon" href="#" />
|
||||||
|
<link rel="stylesheet" href="./style.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xz/fonts@1/serve/metropolis.min.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 id="timer-bar"></div>
|
||||||
|
<div id="timer-label">
|
||||||
|
<span id="ad-label">Ad</span>
|
||||||
|
|
||||||
|
<span id="ads-remaining-label">1 of 3 • </span>
|
||||||
|
<span id="time-remaining-label">1:20</span>
|
||||||
|
</div>
|
||||||
|
<div id="upcoming-ad-warning-container">
|
||||||
|
<div>Ads starting in...</div>
|
||||||
|
<div id="countdown-label">69</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script src="./script.js"></script>
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
////////////////
|
||||||
|
// PARAMETERS //
|
||||||
|
////////////////
|
||||||
|
|
||||||
|
const queryString = window.location.search;
|
||||||
|
const urlParams = new URLSearchParams(queryString);
|
||||||
|
|
||||||
|
let sbDebugMode = true;
|
||||||
|
const sbServerAddress = urlParams.get("address") || "127.0.0.1";
|
||||||
|
const sbServerPort = urlParams.get("port") || "8080";
|
||||||
|
|
||||||
|
///////////////////
|
||||||
|
// PAGE ELEMENTS //
|
||||||
|
///////////////////
|
||||||
|
|
||||||
|
const timerBar = document.getElementById('timer-bar');
|
||||||
|
const adLabel = document.getElementById('ad-label');
|
||||||
|
const timerLabel = document.getElementById('timer-label');
|
||||||
|
const adsRemainingLabel = document.getElementById('ads-remaining-label');
|
||||||
|
const timeRemainingLabel = document.getElementById('time-remaining-label');
|
||||||
|
const upcomingAdWarningContainer = document.getElementById('upcoming-ad-warning-container');
|
||||||
|
const countdownLabel = document.getElementById('countdown-label');
|
||||||
|
|
||||||
|
/////////////
|
||||||
|
// OPTIONS //
|
||||||
|
/////////////
|
||||||
|
|
||||||
|
// Appearance
|
||||||
|
const barColor = urlParams.get("barColor") || "#FFCC00";
|
||||||
|
const barThickness = GetIntParam("barThickness", 4);
|
||||||
|
const barPosition = urlParams.get("barPosition") || "bottom";
|
||||||
|
const timerColor = urlParams.get("timerColor") || "#FFCC00";
|
||||||
|
const timerPosition = urlParams.get("timerPosition") || "top-left";
|
||||||
|
|
||||||
|
// General
|
||||||
|
const showUpcomingAdWarning = GetBooleanParam("showUpcomingAdWarning", true);
|
||||||
|
const warningSeconds = GetIntParam("warningSeconds", 10);
|
||||||
|
|
||||||
|
/////////////////
|
||||||
|
// GLOBAL VARS //
|
||||||
|
/////////////////
|
||||||
|
|
||||||
|
let singleAdLength = 30;
|
||||||
|
let timerBarLocked = false;
|
||||||
|
let timerLabelLocked = false;
|
||||||
|
let upcomingAdWarningLocked = false;
|
||||||
|
|
||||||
|
/////////////////
|
||||||
|
// PAGEE SETUP //
|
||||||
|
/////////////////
|
||||||
|
|
||||||
|
// Set the colors
|
||||||
|
timerBar.style.background = barColor;
|
||||||
|
adLabel.style.background = timerColor;
|
||||||
|
|
||||||
|
// Set the position of the bar
|
||||||
|
switch (barPosition) {
|
||||||
|
case "none":
|
||||||
|
timerBar.style.display = "none";
|
||||||
|
break;
|
||||||
|
case "bottom":
|
||||||
|
timerBar.style.height = barThickness + "px";
|
||||||
|
timerBar.style.bottom = "0px";
|
||||||
|
timerBar.style.left = "0px";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "top":
|
||||||
|
timerBar.style.height = barThickness + "px";
|
||||||
|
timerBar.style.top = "0px";
|
||||||
|
timerBar.style.left = "0px";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "left":
|
||||||
|
timerBar.style.width = barThickness + "px";
|
||||||
|
timerBar.style.bottom = "0px";
|
||||||
|
timerBar.style.left = "0px";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "right":
|
||||||
|
timerBar.style.width = barThickness + "px";
|
||||||
|
timerBar.style.bottom = "0px";
|
||||||
|
timerBar.style.right = "0px";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the position of the timer
|
||||||
|
switch (timerPosition) {
|
||||||
|
case "none":
|
||||||
|
timerLabel.style.display = "none";
|
||||||
|
break;
|
||||||
|
case "top-left":
|
||||||
|
timerLabel.style.top = "0px";
|
||||||
|
timerLabel.style.left = "0px";
|
||||||
|
break;
|
||||||
|
case "top-right":
|
||||||
|
timerLabel.style.top = "0px";
|
||||||
|
timerLabel.style.right = "0px";
|
||||||
|
break;
|
||||||
|
case "bottom-left":
|
||||||
|
timerLabel.style.bottom = "0px";
|
||||||
|
timerLabel.style.left = "0px";
|
||||||
|
break;
|
||||||
|
case "bottom-right":
|
||||||
|
timerLabel.style.bottom = "0px";
|
||||||
|
timerLabel.style.right = "0px";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////
|
||||||
|
// STREAMER.BOT CLIENT //
|
||||||
|
/////////////////////////
|
||||||
|
|
||||||
|
const client = new StreamerbotClient({
|
||||||
|
host: sbServerAddress,
|
||||||
|
port: sbServerPort,
|
||||||
|
|
||||||
|
onConnect: (data) => {
|
||||||
|
console.log(`Streamer.bot successfully connected to ${sbServerAddress}:${sbServerPort}`)
|
||||||
|
console.debug(data);
|
||||||
|
SetConnectionStatus(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
onDisconnect: () => {
|
||||||
|
console.error(`Streamer.bot disconnected from ${sbServerAddress}:${sbServerPort}`)
|
||||||
|
SetConnectionStatus(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('Twitch.AdRun', (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
TwitchAdRun(response.data);
|
||||||
|
})
|
||||||
|
|
||||||
|
client.on('Twitch.UpcomingAd', (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
TwitchUpcomingAd(response.data);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////
|
||||||
|
// TWITCH AD TIMER //
|
||||||
|
/////////////////////
|
||||||
|
|
||||||
|
function TwitchAdRun(data) {
|
||||||
|
const duration = data.length_seconds;
|
||||||
|
|
||||||
|
TimerBarAnimation(duration);
|
||||||
|
TimerLabelAnimation(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TwitchUpcomingAd(data) {
|
||||||
|
// TODO: Need to scrape UpcomingAd data to continue this
|
||||||
|
const warningMinute = 5;
|
||||||
|
|
||||||
|
const delayStart = warningMinute * 60 - warningSeconds;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
UpcomingAdWarning(warningSeconds);
|
||||||
|
}, delayStart * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimerBarAnimation(duration) {
|
||||||
|
// Check if the widget is in the middle of an animation
|
||||||
|
if (timerBarLocked)
|
||||||
|
return;
|
||||||
|
|
||||||
|
timerBarLocked = true;
|
||||||
|
|
||||||
|
// If the bar is top/bottom, animate horizontally
|
||||||
|
// If the bar is left/right, animate vertically
|
||||||
|
switch (barPosition) {
|
||||||
|
case "bottom":
|
||||||
|
case "top":
|
||||||
|
timerBar.style.transition = 'width 0.5s ease-in-out';
|
||||||
|
timerBar.style.width = '100%';
|
||||||
|
setTimeout(() => {
|
||||||
|
timerBar.style.transition = `width ${duration}s linear`;
|
||||||
|
timerBar.style.width = '0%';
|
||||||
|
}, 500);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "left":
|
||||||
|
case "right":
|
||||||
|
timerBar.style.transition = 'height 0.5s ease-in-out';
|
||||||
|
timerBar.style.height = '100%';
|
||||||
|
setTimeout(() => {
|
||||||
|
timerBar.style.transition = `height ${duration}s linear`;
|
||||||
|
timerBar.style.height = '0%';
|
||||||
|
}, 500);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock the widget
|
||||||
|
setTimeout(() => {
|
||||||
|
timerBarLocked = false;
|
||||||
|
}, duration * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimerLabelAnimation(duration) {
|
||||||
|
// Check if the widget is in the middle of an animation
|
||||||
|
if (timerLabelLocked)
|
||||||
|
return;
|
||||||
|
|
||||||
|
timerLabelLocked = true;
|
||||||
|
|
||||||
|
// Calculate starting time
|
||||||
|
let startingTime = duration % singleAdLength;
|
||||||
|
if (startingTime == 0)
|
||||||
|
startingTime = singleAdLength;
|
||||||
|
|
||||||
|
// Estimate how many ads there should be
|
||||||
|
let adsTotal = Math.ceil(duration / singleAdLength);
|
||||||
|
let adsRemaining = 1;
|
||||||
|
|
||||||
|
function updateTimer() {
|
||||||
|
// Show the labels BEFORE decrementing
|
||||||
|
adsRemainingLabel.innerText = `${adsRemaining} of ${adsTotal} • `;
|
||||||
|
timeRemainingLabel.innerText = startingTime.toString().toHHMMSS();
|
||||||
|
timerLabel.style.opacity = 1;
|
||||||
|
|
||||||
|
// Decrement time
|
||||||
|
startingTime--;
|
||||||
|
|
||||||
|
// Check for transitions
|
||||||
|
if (startingTime < 0 && adsRemaining < adsTotal) {
|
||||||
|
adsRemaining++;
|
||||||
|
startingTime = singleAdLength - 1; // -1 since 1 second has already passed
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startingTime < 0 && adsRemaining === adsTotal) {
|
||||||
|
clearInterval(timerThingy);
|
||||||
|
timerLabel.style.opacity = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run immediately
|
||||||
|
updateTimer();
|
||||||
|
|
||||||
|
// Then every second
|
||||||
|
var timerThingy = setInterval(updateTimer, 1000);
|
||||||
|
|
||||||
|
// Unlock the widget
|
||||||
|
setTimeout(() => {
|
||||||
|
timerLabelLocked = false;
|
||||||
|
}, duration * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpcomingAdWarning(warningSeconds) {
|
||||||
|
if (!showUpcomingAdWarning)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Check if the widget is in the middle of an animation
|
||||||
|
if (upcomingAdWarningLocked)
|
||||||
|
return;
|
||||||
|
|
||||||
|
upcomingAdWarningLocked = true;
|
||||||
|
|
||||||
|
let width = upcomingAdWarningContainer.getBoundingClientRect().width;
|
||||||
|
|
||||||
|
// Set the starting position of the countdown box
|
||||||
|
upcomingAdWarningContainer.style.right = -width + "px";
|
||||||
|
countdownLabel.innerHTML = warningSeconds;
|
||||||
|
|
||||||
|
// Slide the countdown box on screen
|
||||||
|
upcomingAdWarningContainer.style.right = "0px";
|
||||||
|
|
||||||
|
// Start the countdown timer
|
||||||
|
let startingTime = warningSeconds;
|
||||||
|
|
||||||
|
var timerThingy = setInterval(function () {
|
||||||
|
startingTime--;
|
||||||
|
countdownLabel.innerText = startingTime;
|
||||||
|
if (startingTime == 0) {
|
||||||
|
clearInterval(timerThingy);
|
||||||
|
upcomingAdWarningContainer.style.right = -width + "px";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
// Unlock the widget
|
||||||
|
setTimeout(() => {
|
||||||
|
upcomingAdWarningLocked = false;
|
||||||
|
}, warningSeconds * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////
|
||||||
|
// HELPER FUNCTIONS //
|
||||||
|
//////////////////////
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
String.prototype.toHHMMSS = function () {
|
||||||
|
var sec_num = parseInt(this, 10); // don't forget the second param
|
||||||
|
var hours = Math.floor(sec_num / 3600);
|
||||||
|
var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||||
|
var seconds = sec_num - (hours * 3600) - (minutes * 60);
|
||||||
|
|
||||||
|
if (hours < 10) { hours = "0" + hours; }
|
||||||
|
//if (minutes < 10) {minutes = "0"+minutes;}
|
||||||
|
if (seconds < 10) { seconds = "0" + seconds; }
|
||||||
|
//return hours+':'+minutes+':'+seconds;
|
||||||
|
return minutes + ':' + seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
function IsNullOrWhitespace(str) {
|
||||||
|
return /^\s*$/.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////
|
||||||
|
// TEST FUNCTIONS //
|
||||||
|
////////////////////
|
||||||
|
|
||||||
|
function testAd() {
|
||||||
|
const data = {
|
||||||
|
"length_seconds": 15,
|
||||||
|
"timestamp": "0001-01-01T00:00:00",
|
||||||
|
"is_automatic": true,
|
||||||
|
"requester_user_id": "54983562",
|
||||||
|
"requester_user_login": "nutty",
|
||||||
|
"requester_user_name": "nutty",
|
||||||
|
"is_test": false
|
||||||
|
}
|
||||||
|
|
||||||
|
TwitchAdRun(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function testUpcomingAdWarning() {
|
||||||
|
UpcomingAdWarning(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////
|
||||||
|
// STREAMER.BOT WEBSOCKET STATUS //
|
||||||
|
///////////////////////////////////
|
||||||
|
|
||||||
|
// This function sets the visibility of the Streamer.bot status label on the overlay
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
{
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"id": "testAd",
|
||||||
|
"label": "Send Test Ad",
|
||||||
|
"description": "",
|
||||||
|
"type": "button",
|
||||||
|
"callFunction": "testAd",
|
||||||
|
"group": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "testUpcomingAdWarning",
|
||||||
|
"label": "Send Test Upcoming Ad Warning",
|
||||||
|
"description": "",
|
||||||
|
"type": "button",
|
||||||
|
"callFunction": "testUpcomingAdWarning",
|
||||||
|
"group": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "barColor",
|
||||||
|
"label": "Bar Color",
|
||||||
|
"description": "",
|
||||||
|
"type": "color",
|
||||||
|
"defaultValue": "#FFCC00",
|
||||||
|
"group": "Appearance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "barThickness",
|
||||||
|
"label": "Bar Thickness",
|
||||||
|
"description": "",
|
||||||
|
"type": "number",
|
||||||
|
"defaultValue": "4",
|
||||||
|
"min": 1,
|
||||||
|
"group": "Appearance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "barPosition",
|
||||||
|
"label": "Bar Position",
|
||||||
|
"description": "",
|
||||||
|
"type": "select",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "none",
|
||||||
|
"label": "None"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "bottom",
|
||||||
|
"label": "Bottom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "top",
|
||||||
|
"label": "Top"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "left",
|
||||||
|
"label": "Left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "right",
|
||||||
|
"label": "Right"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultValue": "bottom",
|
||||||
|
"group": "Appearance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "timerColor",
|
||||||
|
"label": "Timer Color",
|
||||||
|
"description": "",
|
||||||
|
"type": "color",
|
||||||
|
"defaultValue": "#FFCC00",
|
||||||
|
"group": "Appearance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "timerPosition",
|
||||||
|
"label": "Timer Position",
|
||||||
|
"description": "",
|
||||||
|
"type": "select",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "none",
|
||||||
|
"label": "None"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "top-left",
|
||||||
|
"label": "Top Left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "top-right",
|
||||||
|
"label": "Top Right"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "bottom-left",
|
||||||
|
"label": "Bottom Left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "bottom-right",
|
||||||
|
"label": "Bottom Right"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultValue": "top-left",
|
||||||
|
"group": "Appearance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "showUpcomingAdWarning",
|
||||||
|
"label": "Show Upcoming Ad Warning",
|
||||||
|
"description": "",
|
||||||
|
"type": "checkbox",
|
||||||
|
"defaultValue": true,
|
||||||
|
"group": "General"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "warningSeconds",
|
||||||
|
"label": "Warning Time",
|
||||||
|
"description": "Warn X seconds before ad",
|
||||||
|
"type": "number",
|
||||||
|
"defaultValue": "10",
|
||||||
|
"min": 1,
|
||||||
|
"max": 300,
|
||||||
|
"showIf": "showUpcomingAdWarning",
|
||||||
|
"group": "General"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "address",
|
||||||
|
"label": "Address",
|
||||||
|
"description": "Streamer.bot Websocket Server Address",
|
||||||
|
"type": "text",
|
||||||
|
"defaultValue": "127.0.0.1",
|
||||||
|
"group": "Advanced"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "port",
|
||||||
|
"label": "Port",
|
||||||
|
"description": "Streamer.bot Websocket Server Port",
|
||||||
|
"type": "text",
|
||||||
|
"defaultValue": "8080",
|
||||||
|
"group": "Advanced"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -3,14 +3,20 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
/* color: white;
|
||||||
|
text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
|
||||||
|
overflow-wrap: break-word; */
|
||||||
|
}
|
||||||
|
|
||||||
#statusContainer {
|
#statusContainer {
|
||||||
text-transform: uppercase;
|
font-weight: 500;
|
||||||
font-family: 'Metropolis';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
width: 400px;
|
|
||||||
max-width: fit-content;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: #D12025;
|
background-color: #D12025;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -22,25 +28,28 @@
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
#hugeTittiesContainer {
|
#timer-bar {
|
||||||
font-family: 'Metropolis';
|
|
||||||
font-weight: bold;
|
|
||||||
background-color: rgba(0, 0, 0, .4);
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 10px;
|
|
||||||
position: absolute;
|
|
||||||
margin: 50px;
|
|
||||||
opacity: 0;
|
|
||||||
display: table;
|
|
||||||
}
|
|
||||||
|
|
||||||
#timerBar {
|
|
||||||
width: 0px;
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
background-color: #ffcc00;
|
background-color: #ffcc00;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
#adLabel {
|
#timer-label {
|
||||||
|
font-family: 'Metropolis';
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: rgba(0, 0, 0, .4);
|
||||||
|
color: white;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
position: absolute;
|
||||||
|
margin: 50px;
|
||||||
|
display: table;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ad-label {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
max-width: fit-content;
|
max-width: fit-content;
|
||||||
@@ -53,17 +62,7 @@
|
|||||||
display: table-cell;
|
display: table-cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
#timerLabel {
|
#upcoming-ad-warning-container {
|
||||||
font-size: 20px;
|
|
||||||
max-width: fit-content;
|
|
||||||
text-align: center;
|
|
||||||
display: inline-block;
|
|
||||||
color: white;
|
|
||||||
padding: 10px 0px 10px 10px;
|
|
||||||
display: table-cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
#midRollContainer {
|
|
||||||
font-family: 'Metropolis';
|
font-family: 'Metropolis';
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
max-width: fit-content;
|
max-width: fit-content;
|
||||||
@@ -77,9 +76,10 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 15%;
|
bottom: 15%;
|
||||||
right: -100%;
|
right: -100%;
|
||||||
|
transition: right 0.5s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
#midRollCountdownContainer {
|
#countdown-label {
|
||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin: 10px 0px 0px 0px;
|
margin: 10px 0px 0px 0px;
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<link rel="shortcut icon" href="#" />
|
|
||||||
<link rel="stylesheet" href="./style.css" />
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xz/fonts@1/serve/metropolis.min.css" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="statusContainer">Connecting...</div>
|
|
||||||
<div id="mainContainer">
|
|
||||||
<div id="timerBar"></div>
|
|
||||||
</div>
|
|
||||||
<div id="hugeTittiesContainer">
|
|
||||||
<div id="adLabel">
|
|
||||||
Ad
|
|
||||||
</div>
|
|
||||||
<div id="timerLabel">
|
|
||||||
<span id="adsRemainingContainer"></span> • <span id="timerContainer"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="midRollContainer">
|
|
||||||
<div id="midRollStartingInLabel">Ads starting in...</div>
|
|
||||||
<div id="midRollCountdownContainer"></div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js'></script>
|
|
||||||
<script src="./script.js"></script>
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
////////////////
|
|
||||||
// PARAMETERS //
|
|
||||||
////////////////
|
|
||||||
|
|
||||||
const queryString = window.location.search;
|
|
||||||
const urlParams = new URLSearchParams(queryString);
|
|
||||||
|
|
||||||
let sbDebugMode = true;
|
|
||||||
const sbServerAddress = urlParams.get("address") || "127.0.0.1";
|
|
||||||
const sbServerPort = urlParams.get("port") || "8080";
|
|
||||||
const lineThickness = urlParams.get("lineThickness") || 4;
|
|
||||||
const barPosition = urlParams.get("barPosition") || "bottom"; // none, bottom, top, left, right
|
|
||||||
const timerPosition = urlParams.get("timerPosition") || "topLeft"; // none, topLeft, topRight, bottomLeft, bottomRight
|
|
||||||
const showMidRollCountdown = urlParams.get("timerPosition") || true;
|
|
||||||
|
|
||||||
/////////////////
|
|
||||||
// GLOBAL VARS //
|
|
||||||
/////////////////
|
|
||||||
|
|
||||||
let ws;
|
|
||||||
let singleAdLength = 30;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////
|
|
||||||
// SRTEAMER.BOT WEBSOCKET SERVER //
|
|
||||||
///////////////////////////////////
|
|
||||||
|
|
||||||
// This is the main function that connects to the Streamer.bot websocket server
|
|
||||||
function connectws() {
|
|
||||||
if ("WebSocket" in window) {
|
|
||||||
ws = new WebSocket("ws://" + sbServerAddress + ":" + sbServerPort + "/");
|
|
||||||
|
|
||||||
// Reconnect
|
|
||||||
ws.onclose = function () {
|
|
||||||
SetConnectionStatus(false);
|
|
||||||
setTimeout(connectws, 5000);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Connect
|
|
||||||
ws.onopen = async function () {
|
|
||||||
SetConnectionStatus(true);
|
|
||||||
|
|
||||||
console.log("Subscribe to events");
|
|
||||||
ws.send(
|
|
||||||
JSON.stringify({
|
|
||||||
request: "Subscribe",
|
|
||||||
id: "subscribe-events-id",
|
|
||||||
events: {
|
|
||||||
// This is the list of Streamer.bot websocket events to subscribe to
|
|
||||||
// See full list of events here:
|
|
||||||
// https://wiki.streamer.bot/en/Servers-Clients/WebSocket-Server/Requests
|
|
||||||
twitch: [
|
|
||||||
"AdRun",
|
|
||||||
"AdMidRoll"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
ws.onmessage = function (event) {
|
|
||||||
// Grab message and parse JSON
|
|
||||||
const msg = event.data;
|
|
||||||
const wsdata = JSON.parse(msg);
|
|
||||||
|
|
||||||
if (typeof wsdata.event == "undefined") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print data to log for debugging purposes
|
|
||||||
if (sbDebugMode)
|
|
||||||
{
|
|
||||||
console.log(wsdata.data);
|
|
||||||
console.log(wsdata.event.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for events to trigger
|
|
||||||
// See documentation for all events here:
|
|
||||||
// https://wiki.streamer.bot/en/Servers-Clients/WebSocket-Server/Events
|
|
||||||
switch (wsdata.event.source) {
|
|
||||||
|
|
||||||
// Twitch Events
|
|
||||||
case 'Twitch':
|
|
||||||
switch (wsdata.event.type) {
|
|
||||||
case ('AdRun'):
|
|
||||||
AdRun(wsdata.data);
|
|
||||||
break;
|
|
||||||
case ('AdMidRoll'):
|
|
||||||
AdMidRoll(wsdata.data);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////
|
|
||||||
// TWITCH AD OVERLAY //
|
|
||||||
///////////////////////
|
|
||||||
|
|
||||||
function AdRun(data) {
|
|
||||||
if (data.length_seconds <= 0)
|
|
||||||
{
|
|
||||||
TimerBarAnimation(singleAdLength);
|
|
||||||
HugeTittiesAnimation(singleAdLength);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TimerBarAnimation(data.length_seconds);
|
|
||||||
HugeTittiesAnimation(data.length_seconds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function AdMidRoll(data) {
|
|
||||||
if (!showMidRollCountdown)
|
|
||||||
return;
|
|
||||||
|
|
||||||
//MidRollAnimation(data.length);
|
|
||||||
MidRollAnimation(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TimerBarAnimation(adLength) {
|
|
||||||
let timerBar = document.getElementById("timerBar");
|
|
||||||
|
|
||||||
switch (barPosition) {
|
|
||||||
case "none":
|
|
||||||
timerBar.style.display = "none";
|
|
||||||
break;
|
|
||||||
case "bottom":
|
|
||||||
timerBar.style.height = lineThickness + "px";
|
|
||||||
timerBar.style.bottom = "0px";
|
|
||||||
timerBar.style.left = "0px";
|
|
||||||
|
|
||||||
// Start Animation
|
|
||||||
tl = new TimelineMax();
|
|
||||||
tl
|
|
||||||
.to(timerBar, 0.5, { width: window.innerWidth + "px", ease: Cubic.ease })
|
|
||||||
.to(timerBar, adLength, { width: "0px", ease: Linear.easeNone })
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "top":
|
|
||||||
timerBar.style.height = lineThickness + "px";
|
|
||||||
timerBar.style.top = "0px";
|
|
||||||
timerBar.style.left = "0px";
|
|
||||||
|
|
||||||
// Start Animation
|
|
||||||
tl = new TimelineMax();
|
|
||||||
tl
|
|
||||||
.to(timerBar, 0.5, { width: window.innerWidth + "px", ease: Cubic.ease })
|
|
||||||
.to(timerBar, adLength, { width: "0px", ease: Linear.easeNone })
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "left":
|
|
||||||
timerBar.style.width = lineThickness + "px";
|
|
||||||
timerBar.style.bottom = "0px";
|
|
||||||
timerBar.style.left = "0px";
|
|
||||||
|
|
||||||
// Start Animation
|
|
||||||
tl = new TimelineMax();
|
|
||||||
tl
|
|
||||||
.to(timerBar, 0.5, { height: window.innerHeight + "px", ease: Cubic.ease })
|
|
||||||
.to(timerBar, adLength, { height: "0px", ease: Linear.easeNone })
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "right":
|
|
||||||
timerBar.style.width = lineThickness + "px";
|
|
||||||
timerBar.style.bottom = "0px";
|
|
||||||
timerBar.style.right = "0px";
|
|
||||||
|
|
||||||
// Start Animation
|
|
||||||
tl = new TimelineMax();
|
|
||||||
tl
|
|
||||||
.to(timerBar, 0.5, { height: window.innerHeight + "px", ease: Cubic.ease })
|
|
||||||
.to(timerBar, adLength, { height: "0px", ease: Linear.easeNone })
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function HugeTittiesAnimation(adLength) {
|
|
||||||
|
|
||||||
let hugeTittiesContainer = document.getElementById("hugeTittiesContainer");
|
|
||||||
|
|
||||||
switch (timerPosition) {
|
|
||||||
case "none":
|
|
||||||
hugeTittiesContainer.style.display = "none";
|
|
||||||
break;
|
|
||||||
case "topLeft":
|
|
||||||
hugeTittiesContainer.style.top = "0px";
|
|
||||||
hugeTittiesContainer.style.left = "0px";
|
|
||||||
break;
|
|
||||||
case "topRight":
|
|
||||||
hugeTittiesContainer.style.top = "0px";
|
|
||||||
hugeTittiesContainer.style.right = "0px";
|
|
||||||
break;
|
|
||||||
case "bottomLeft":
|
|
||||||
hugeTittiesContainer.style.bottom = "0px";
|
|
||||||
hugeTittiesContainer.style.left = "0px";
|
|
||||||
break;
|
|
||||||
case "bottomRight":
|
|
||||||
hugeTittiesContainer.style.bottom = "0px";
|
|
||||||
hugeTittiesContainer.style.right = "0px";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate starting time
|
|
||||||
let startingTime = adLength % singleAdLength;
|
|
||||||
if (startingTime == 0)
|
|
||||||
startingTime = singleAdLength;
|
|
||||||
|
|
||||||
// Estimate how many ads there should be
|
|
||||||
let adsTotal = Math.ceil(adLength / singleAdLength);
|
|
||||||
let adsRemaining = 1;
|
|
||||||
|
|
||||||
// Start the countdown timer
|
|
||||||
let adsRemainingContainer = document.getElementById("adsRemainingContainer");
|
|
||||||
let timerContainer = document.getElementById("timerContainer");
|
|
||||||
|
|
||||||
var timerThingy = setInterval(function () {
|
|
||||||
startingTime--;
|
|
||||||
if (startingTime == 0 && adsRemaining < adsTotal) {
|
|
||||||
adsRemaining++;
|
|
||||||
startingTime = singleAdLength;
|
|
||||||
}
|
|
||||||
if (startingTime == 0 && adsRemaining == adsTotal) {
|
|
||||||
clearInterval(timerThingy);
|
|
||||||
SetVisibility(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
adsRemainingContainer.innerText = adsRemaining + " of " + adsTotal;
|
|
||||||
timerContainer.innerText = startingTime.toString().toHHMMSS();
|
|
||||||
|
|
||||||
// Show the widget
|
|
||||||
SetVisibility(true);
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
function MidRollAnimation(countdownLength) {
|
|
||||||
let midRollContainer = document.getElementById("midRollContainer");
|
|
||||||
let midRollCountdownContainer = document.getElementById("midRollCountdownContainer");
|
|
||||||
let width = midRollContainer.getBoundingClientRect().width;
|
|
||||||
|
|
||||||
// Set the starting position of the countdown box
|
|
||||||
midRollContainer.style.right = -width + "px";
|
|
||||||
midRollCountdownContainer.innerHTML = countdownLength;
|
|
||||||
|
|
||||||
// Slide the countdown box on screen
|
|
||||||
ShowMidRollCountdown(true);
|
|
||||||
|
|
||||||
// Start the countdown timer
|
|
||||||
let startingTime = countdownLength;
|
|
||||||
|
|
||||||
var timerThingy = setInterval(function () {
|
|
||||||
startingTime--;
|
|
||||||
midRollCountdownContainer.innerText = startingTime;
|
|
||||||
if (startingTime == 0) {
|
|
||||||
clearInterval(timerThingy);
|
|
||||||
ShowMidRollCountdown(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SetVisibility(isVisible) {
|
|
||||||
let hugeTittiesContainer = document.getElementById("hugeTittiesContainer");
|
|
||||||
|
|
||||||
var tl = new TimelineMax();
|
|
||||||
tl
|
|
||||||
.to(hugeTittiesContainer, 0.5, { opacity: isVisible, ease: Linear.easeNone });
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShowMidRollCountdown(isVisible) {
|
|
||||||
let midRollContainer = document.getElementById("midRollContainer");
|
|
||||||
let width = midRollContainer.getBoundingClientRect().width;
|
|
||||||
|
|
||||||
var tl = new TimelineMax();
|
|
||||||
|
|
||||||
if (isVisible)
|
|
||||||
{
|
|
||||||
tl
|
|
||||||
.to(midRollContainer, 0.5, { right: "-10px", ease: Power1.easeInOut })
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
tl
|
|
||||||
.to(midRollContainer, 0.5, { right: -width + "px", ease: Power1.easeInOut })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//////////////////////
|
|
||||||
// HELPER FUNCTIONS //
|
|
||||||
//////////////////////
|
|
||||||
|
|
||||||
String.prototype.toHHMMSS = function () {
|
|
||||||
var sec_num = parseInt(this, 10); // don't forget the second param
|
|
||||||
var hours = Math.floor(sec_num / 3600);
|
|
||||||
var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
|
||||||
var seconds = sec_num - (hours * 3600) - (minutes * 60);
|
|
||||||
|
|
||||||
if (hours < 10) { hours = "0" + hours; }
|
|
||||||
//if (minutes < 10) {minutes = "0"+minutes;}
|
|
||||||
if (seconds < 10) { seconds = "0" + seconds; }
|
|
||||||
//return hours+':'+minutes+':'+seconds;
|
|
||||||
return minutes + ':' + seconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
function IsNullOrWhitespace(str) {
|
|
||||||
return /^\s*$/.test(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////
|
|
||||||
// STREAMER.BOT WEBSOCKET STATUS //
|
|
||||||
///////////////////////////////////
|
|
||||||
|
|
||||||
function SetConnectionStatus(connected) {
|
|
||||||
let statusContainer = document.getElementById("statusContainer");
|
|
||||||
if (connected) {
|
|
||||||
statusContainer.style.background = "#2FB774";
|
|
||||||
statusContainer.innerText = "Connected!";
|
|
||||||
var tl = new TimelineMax();
|
|
||||||
tl
|
|
||||||
.to(statusContainer, 2, { opacity: 0, ease: Linear.easeNone })
|
|
||||||
//.call(removeElement, [div]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
statusContainer.style.background = "#D12025";
|
|
||||||
statusContainer.innerText = "Connecting...";
|
|
||||||
statusContainer.style.opacity = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectws();
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"settings": [
|
|
||||||
{
|
|
||||||
"id": "address",
|
|
||||||
"label": "Address",
|
|
||||||
"description": "Streamer.bot Websocket Server Address",
|
|
||||||
"type": "text",
|
|
||||||
"defaultValue": "127.0.0.1",
|
|
||||||
"group": "Advanced"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "port",
|
|
||||||
"label": "Port",
|
|
||||||
"description": "Streamer.bot Websocket Server Port",
|
|
||||||
"type": "text",
|
|
||||||
"defaultValue": "8080",
|
|
||||||
"group": "Advanced"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user