diff --git a/twitch-ad-countdown-timer/index.html b/twitch-ad-countdown-timer/index.html
new file mode 100644
index 0000000..e3e8d75
--- /dev/null
+++ b/twitch-ad-countdown-timer/index.html
@@ -0,0 +1,28 @@
+
+
+
+ Connecting...
+
+
+
+ Ad
+
+ 1 of 3 •
+ 1:20
+
+
+
Ads starting in...
+
69
+
+
+
+
+
\ No newline at end of file
diff --git a/twitch-ad-countdown-timer/script.js b/twitch-ad-countdown-timer/script.js
new file mode 100644
index 0000000..191ecf1
--- /dev/null
+++ b/twitch-ad-countdown-timer/script.js
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/twitch-ad-timer/settings/index.html b/twitch-ad-countdown-timer/settings/index.html
similarity index 100%
rename from twitch-ad-timer/settings/index.html
rename to twitch-ad-countdown-timer/settings/index.html
diff --git a/twitch-ad-timer/settings/script.js b/twitch-ad-countdown-timer/settings/script.js
similarity index 100%
rename from twitch-ad-timer/settings/script.js
rename to twitch-ad-countdown-timer/settings/script.js
diff --git a/twitch-ad-countdown-timer/settings/settings.json b/twitch-ad-countdown-timer/settings/settings.json
new file mode 100644
index 0000000..fdb7e26
--- /dev/null
+++ b/twitch-ad-countdown-timer/settings/settings.json
@@ -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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/twitch-ad-timer/style.css b/twitch-ad-countdown-timer/style.css
similarity index 72%
rename from twitch-ad-timer/style.css
rename to twitch-ad-countdown-timer/style.css
index 57bf3ae..b42d350 100644
--- a/twitch-ad-timer/style.css
+++ b/twitch-ad-countdown-timer/style.css
@@ -3,14 +3,20 @@
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 {
- text-transform: uppercase;
- font-family: 'Metropolis';
- font-style: italic;
- font-weight: bold;
+ font-weight: 500;
font-size: 30px;
- width: 400px;
- max-width: fit-content;
text-align: center;
background-color: #D12025;
color: white;
@@ -22,25 +28,28 @@
transform: translate(-50%, -50%);
}
-#hugeTittiesContainer {
- 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 {
+#timer-bar {
width: 0px;
+ height: 0px;
background-color: #ffcc00;
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-weight: bold;
max-width: fit-content;
@@ -53,17 +62,7 @@
display: table-cell;
}
-#timerLabel {
- font-size: 20px;
- max-width: fit-content;
- text-align: center;
- display: inline-block;
- color: white;
- padding: 10px 0px 10px 10px;
- display: table-cell;
-}
-
-#midRollContainer {
+#upcoming-ad-warning-container {
font-family: 'Metropolis';
font-size: 20px;
max-width: fit-content;
@@ -77,9 +76,10 @@
position: absolute;
bottom: 15%;
right: -100%;
+ transition: right 0.5s ease-in-out;
}
-#midRollCountdownContainer {
+#countdown-label {
font-size: 40px;
font-weight: bold;
margin: 10px 0px 0px 0px;
diff --git a/twitch-ad-timer/index.html b/twitch-ad-timer/index.html
deleted file mode 100644
index 7d541eb..0000000
--- a/twitch-ad-timer/index.html
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
- Connecting...
-