diff --git a/twitch-ad-timer/index.html b/twitch-ad-timer/index.html
new file mode 100644
index 0000000..7d541eb
--- /dev/null
+++ b/twitch-ad-timer/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+ Connecting...
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/twitch-ad-timer/script.js b/twitch-ad-timer/script.js
new file mode 100644
index 0000000..513f5e4
--- /dev/null
+++ b/twitch-ad-timer/script.js
@@ -0,0 +1,340 @@
+////////////////
+// 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();
\ No newline at end of file
diff --git a/twitch-ad-timer/style.css b/twitch-ad-timer/style.css
new file mode 100644
index 0000000..57bf3ae
--- /dev/null
+++ b/twitch-ad-timer/style.css
@@ -0,0 +1,86 @@
+* {
+ margin: 0;
+ padding: 0;
+}
+
+#statusContainer {
+ text-transform: uppercase;
+ font-family: 'Metropolis';
+ font-style: italic;
+ font-weight: bold;
+ font-size: 30px;
+ width: 400px;
+ max-width: fit-content;
+ text-align: center;
+ background-color: #D12025;
+ color: white;
+ padding: 10px;
+ border-radius: 10px;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ 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 {
+ width: 0px;
+ background-color: #ffcc00;
+ position: absolute;
+}
+
+#adLabel {
+ font-size: 20px;
+ font-weight: bold;
+ max-width: fit-content;
+ text-align: center;
+ display: inline-block;
+ background-color: #F9BE2A;
+ color: #1C1B22;
+ padding: 10px;
+ border-radius: 10px;
+ 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 {
+ font-family: 'Metropolis';
+ font-size: 20px;
+ max-width: fit-content;
+ text-align: center;
+ display: inline-block;
+ background-color: #000000CC;
+ color: #FFFFFF;
+ padding: 10px 20px;
+ border-radius: 10px;
+ display: table-cell;
+ position: absolute;
+ bottom: 15%;
+ right: -100%;
+}
+
+#midRollCountdownContainer {
+ font-size: 40px;
+ font-weight: bold;
+ margin: 10px 0px 0px 0px;
+}
\ No newline at end of file