+
+ From 6c53d973f7e6556c77bd743b887dd9211e579e3e Mon Sep 17 00:00:00 2001 From: nuttylmao <76841561+nuttylmao@users.noreply.github.com> Date: Sun, 23 Mar 2025 22:04:07 +1100 Subject: [PATCH] Add files via upload --- apple-music-widget-compact/index.html | 47 + apple-music-widget-compact/script.js | 230 + apple-music-widget-compact/style.css | 181 + apple-music-widget/index.html | 47 + apple-music-widget/script.js | 229 + apple-music-widget/style.css | 177 + clock-widget/index.html | 8 + clock-widget/moment.js | 5688 +++++++++++++++++ clock-widget/script.js | 19 + clock-widget/style.css | 19 + .../icons/badges/youtube-broadcaster.svg | 1 + .../icons/badges/youtube-member.svg | 1 + .../icons/badges/youtube-moderator.svg | 1 + .../icons/badges/youtube-verified.svg | 1 + horizontal-chat/icons/platforms/twitch.png | Bin 0 -> 4015 bytes horizontal-chat/icons/platforms/youtube.png | Bin 0 -> 8618 bytes horizontal-chat/index.html | 38 + horizontal-chat/script.js | 881 +++ horizontal-chat/settings/index.html | 64 + horizontal-chat/settings/script.js | 29 + horizontal-chat/settings/settings.json | 200 + horizontal-chat/style.css | 303 + .../icons/badges/youtube-broadcaster.svg | 1 + .../icons/badges/youtube-member.svg | 1 + .../icons/badges/youtube-moderator.svg | 1 + .../icons/badges/youtube-verified.svg | 1 + multichat-overlay/icons/platforms/twitch.png | Bin 0 -> 4015 bytes multichat-overlay/icons/platforms/youtube.png | Bin 0 -> 8618 bytes multichat-overlay/index.html | 70 + multichat-overlay/script.js | 1406 ++++ multichat-overlay/settings/index.html | 64 + multichat-overlay/settings/script.js | 29 + multichat-overlay/settings/settings.json | 271 + multichat-overlay/style.css | 245 + multistream-title-updater/connected.svg | 7 + multistream-title-updater/disconnected.svg | 7 + multistream-title-updater/icons.afdesign | Bin 0 -> 437973 bytes multistream-title-updater/index.html | 81 + multistream-title-updater/info.svg | 5 + multistream-title-updater/requiredActions.txt | 4 + multistream-title-updater/script.js | 444 ++ multistream-title-updater/style.css | 201 + multistream-title-updater/twitch.png | Bin 0 -> 56251 bytes multistream-title-updater/youtube.png | Bin 0 -> 66870 bytes obs-info-hud/frames/2x1.png | Bin 0 -> 38579 bytes obs-info-hud/frames/tiktok.png | Bin 0 -> 259918 bytes obs-info-hud/frames/vertical.png | Bin 0 -> 42059 bytes obs-info-hud/frames/youtube-short-live.png | Bin 0 -> 618778 bytes obs-info-hud/frames/youtube-short.png | Bin 0 -> 208196 bytes obs-info-hud/icons/mic-mute.png | Bin 0 -> 20485 bytes obs-info-hud/index.html | 80 + obs-info-hud/script.js | 492 ++ obs-info-hud/style.css | 257 + scumbag-sub-goal-widget/index.html | 8 + scumbag-sub-goal-widget/script.js | 36 + scumbag-sub-goal-widget/style.css | 16 + simple-twitch-metrics/index.html | 20 + simple-twitch-metrics/script.js | 38 + simple-twitch-metrics/style.css | 25 + spotify-widget-compact/configure/index.html | 49 + spotify-widget-compact/configure/logo.webp | Bin 0 -> 28410 bytes spotify-widget-compact/configure/script.js | 131 + spotify-widget-compact/configure/style.css | 104 + .../images/placeholder-album-art.afdesign | Bin 0 -> 74942 bytes .../images/placeholder-album-art.png | Bin 0 -> 63587 bytes spotify-widget-compact/index.html | 46 + spotify-widget-compact/script.js | 270 + spotify-widget-compact/style.css | 191 + spotify-widget/configure/index.html | 49 + spotify-widget/configure/logo.webp | Bin 0 -> 28410 bytes spotify-widget/configure/script.js | 131 + spotify-widget/configure/style.css | 104 + .../images/placeholder-album-art.afdesign | Bin 0 -> 74942 bytes .../images/placeholder-album-art.png | Bin 0 -> 63587 bytes spotify-widget/index.html | 46 + spotify-widget/script.js | 269 + spotify-widget/style.css | 192 + twitch-uptime-widget/index.html | 7 + twitch-uptime-widget/script.js | 40 + twitch-uptime-widget/style.css | 16 + widget-customizer/index.html | 40 + widget-customizer/script.js | 237 + widget-customizer/style.css | 269 + .../configure/index.html | 47 + .../configure/logo.png | Bin 0 -> 62699 bytes .../configure/script.js | 110 + .../configure/style.css | 115 + youtube-music-widget-compact/index.html | 46 + youtube-music-widget-compact/script.js | 233 + youtube-music-widget-compact/style.css | 183 + youtube-music-widget/configure/index.html | 47 + youtube-music-widget/configure/logo.png | Bin 0 -> 62699 bytes youtube-music-widget/configure/script.js | 110 + youtube-music-widget/configure/style.css | 115 + youtube-music-widget/index.html | 46 + youtube-music-widget/script.js | 231 + youtube-music-widget/style.css | 177 + 97 files changed, 15625 insertions(+) create mode 100644 apple-music-widget-compact/index.html create mode 100644 apple-music-widget-compact/script.js create mode 100644 apple-music-widget-compact/style.css create mode 100644 apple-music-widget/index.html create mode 100644 apple-music-widget/script.js create mode 100644 apple-music-widget/style.css create mode 100644 clock-widget/index.html create mode 100644 clock-widget/moment.js create mode 100644 clock-widget/script.js create mode 100644 clock-widget/style.css create mode 100644 horizontal-chat/icons/badges/youtube-broadcaster.svg create mode 100644 horizontal-chat/icons/badges/youtube-member.svg create mode 100644 horizontal-chat/icons/badges/youtube-moderator.svg create mode 100644 horizontal-chat/icons/badges/youtube-verified.svg create mode 100644 horizontal-chat/icons/platforms/twitch.png create mode 100644 horizontal-chat/icons/platforms/youtube.png create mode 100644 horizontal-chat/index.html create mode 100644 horizontal-chat/script.js create mode 100644 horizontal-chat/settings/index.html create mode 100644 horizontal-chat/settings/script.js create mode 100644 horizontal-chat/settings/settings.json create mode 100644 horizontal-chat/style.css create mode 100644 multichat-overlay/icons/badges/youtube-broadcaster.svg create mode 100644 multichat-overlay/icons/badges/youtube-member.svg create mode 100644 multichat-overlay/icons/badges/youtube-moderator.svg create mode 100644 multichat-overlay/icons/badges/youtube-verified.svg create mode 100644 multichat-overlay/icons/platforms/twitch.png create mode 100644 multichat-overlay/icons/platforms/youtube.png create mode 100644 multichat-overlay/index.html create mode 100644 multichat-overlay/script.js create mode 100644 multichat-overlay/settings/index.html create mode 100644 multichat-overlay/settings/script.js create mode 100644 multichat-overlay/settings/settings.json create mode 100644 multichat-overlay/style.css create mode 100644 multistream-title-updater/connected.svg create mode 100644 multistream-title-updater/disconnected.svg create mode 100644 multistream-title-updater/icons.afdesign create mode 100644 multistream-title-updater/index.html create mode 100644 multistream-title-updater/info.svg create mode 100644 multistream-title-updater/requiredActions.txt create mode 100644 multistream-title-updater/script.js create mode 100644 multistream-title-updater/style.css create mode 100644 multistream-title-updater/twitch.png create mode 100644 multistream-title-updater/youtube.png create mode 100644 obs-info-hud/frames/2x1.png create mode 100644 obs-info-hud/frames/tiktok.png create mode 100644 obs-info-hud/frames/vertical.png create mode 100644 obs-info-hud/frames/youtube-short-live.png create mode 100644 obs-info-hud/frames/youtube-short.png create mode 100644 obs-info-hud/icons/mic-mute.png create mode 100644 obs-info-hud/index.html create mode 100644 obs-info-hud/script.js create mode 100644 obs-info-hud/style.css create mode 100644 scumbag-sub-goal-widget/index.html create mode 100644 scumbag-sub-goal-widget/script.js create mode 100644 scumbag-sub-goal-widget/style.css create mode 100644 simple-twitch-metrics/index.html create mode 100644 simple-twitch-metrics/script.js create mode 100644 simple-twitch-metrics/style.css create mode 100644 spotify-widget-compact/configure/index.html create mode 100644 spotify-widget-compact/configure/logo.webp create mode 100644 spotify-widget-compact/configure/script.js create mode 100644 spotify-widget-compact/configure/style.css create mode 100644 spotify-widget-compact/images/placeholder-album-art.afdesign create mode 100644 spotify-widget-compact/images/placeholder-album-art.png create mode 100644 spotify-widget-compact/index.html create mode 100644 spotify-widget-compact/script.js create mode 100644 spotify-widget-compact/style.css create mode 100644 spotify-widget/configure/index.html create mode 100644 spotify-widget/configure/logo.webp create mode 100644 spotify-widget/configure/script.js create mode 100644 spotify-widget/configure/style.css create mode 100644 spotify-widget/images/placeholder-album-art.afdesign create mode 100644 spotify-widget/images/placeholder-album-art.png create mode 100644 spotify-widget/index.html create mode 100644 spotify-widget/script.js create mode 100644 spotify-widget/style.css create mode 100644 twitch-uptime-widget/index.html create mode 100644 twitch-uptime-widget/script.js create mode 100644 twitch-uptime-widget/style.css create mode 100644 widget-customizer/index.html create mode 100644 widget-customizer/script.js create mode 100644 widget-customizer/style.css create mode 100644 youtube-music-widget-compact/configure/index.html create mode 100644 youtube-music-widget-compact/configure/logo.png create mode 100644 youtube-music-widget-compact/configure/script.js create mode 100644 youtube-music-widget-compact/configure/style.css create mode 100644 youtube-music-widget-compact/index.html create mode 100644 youtube-music-widget-compact/script.js create mode 100644 youtube-music-widget-compact/style.css create mode 100644 youtube-music-widget/configure/index.html create mode 100644 youtube-music-widget/configure/logo.png create mode 100644 youtube-music-widget/configure/script.js create mode 100644 youtube-music-widget/configure/style.css create mode 100644 youtube-music-widget/index.html create mode 100644 youtube-music-widget/script.js create mode 100644 youtube-music-widget/style.css diff --git a/apple-music-widget-compact/index.html b/apple-music-widget-compact/index.html new file mode 100644 index 0000000..9d511de --- /dev/null +++ b/apple-music-widget-compact/index.html @@ -0,0 +1,47 @@ + + +
+ + + + + + + + +
+
+
+
+
+
+ u?9nTUH#rU1
zoMN`89227xm*VGiPuxJXZiZ)>9VFe;9}U9k#7 u?9nTUH#rU1
zoMN`89227xm*VGiPuxJXZiZ)>9VFe;9}U9k#7zT?pp3e5W}oBHa>hGDH&|IL{Bs(f@}Z;@@j=wf~%ZBSLd_
Q-%<~M%IajrUmn;0A5D{si2wiq
literal 0
HcmV?d00001
diff --git a/horizontal-chat/index.html b/horizontal-chat/index.html
new file mode 100644
index 0000000..3dcfc33
--- /dev/null
+++ b/horizontal-chat/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
`;
+ platformDiv.innerHTML = platformElements;
+ }
+
+
+ // 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 = ``;
+ messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${data.emotes[i].name}\\b`), emoteElement);
+ }
+
+ // Render cheermotes
+ for (i in data.cheerEmotes) {
+ // const cheerEmoteElement = `
`;
+ // messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${data.cheerEmotes[i].name}\\b`), cheerEmoteElement);
+ const bits = data.cheerEmotes[i].bits;
+ const imageUrl = data.cheerEmotes[i].imageUrl;
+ const name = data.cheerEmotes[i].name;
+ const cheerEmoteElement = `
`;
+ const bitsElements = `${bits}`
+ messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements);
+ }
+
+ // Render avatars
+ if (showAvatar) {
+ const username = data.message.username;
+ const avatarURL = await GetAvatar(username);
+ const avatar = new Image();
+ avatar.src = avatarURL;
+ avatar.classList.add("avatar");
+ avatarDiv.appendChild(avatar);
+ }
+
+ // Hide the header if the same username sends a message twice in a row
+ const messageList = document.getElementById("messageList");
+ if (messageList.children.length > 0) {
+ const lastPlatform = messageList.lastChild.dataset.platform;
+ const lastUserId = messageList.lastChild.dataset.userId;
+ if (lastPlatform == "twitch" && lastUserId == data.user.id)
+ userInfoDiv.style.display = "none";
+ }
+
+ AddMessageItem(instance, data.message.msgId, 'twitch', data.user.id);
+}
+
+async function TwitchAnnouncement(data) {
+ if (!showTwitchAnnouncements)
+ return;
+
+ let background = null;
+
+ // Set the card background colors
+ switch (data.announcementColor) {
+ case "BLUE":
+ background = 'announcementBlue';
+ break;
+ case "GREEN":
+ background = 'announcementGreen';
+ break;
+ case "ORANGE":
+ background = 'announcementOrange';
+ break;
+ case "PURPLE":
+ background = 'announcementPurple';
+ break;
+ }
+
+ let message = data.text;
+
+ // Render emotes
+ for (i in data.parts) {
+ if (data.parts[i].type == `emote`) {
+ const emoteElement = `
`;
+ message = message.replace(new RegExp(`\\b${data.parts[i].text}\\b`), emoteElement);
+ }
+ }
+
+ ShowAlert(message, background);
+}
+
+async function TwitchSub(data) {
+ if (!showTwitchSubs)
+ return;
+
+ const username = data.user.name;
+ const subTier = data.sub_tier;
+ const isPrime = data.is_prime;
+
+ let message = '';
+
+ if (!isPrime)
+ message = `${username} subscribed with Tier ${subTier.charAt(0)}`;
+ else
+ message = `${username} used their Prime Sub`, 'twitch';
+
+ ShowAlert(message, 'twitch');
+}
+
+async function TwitchResub(data) {
+ if (!showTwitchSubs)
+ return;
+
+ const username = data.user.name;
+ const subTier = data.subTier;
+ const isPrime = data.isPrime;
+ const cumulativeMonths = data.cumulativeMonths;
+
+ let message = '';
+
+ if (!isPrime)
+ message = `${username} resubscribed with Tier ${subTier.charAt(0)} (${cumulativeMonths} months)`;
+ else
+ message = `${username} used their Prime Sub (${cumulativeMonths} months)`;
+
+ ShowAlert(message, 'twitch');
+}
+
+async function TwitchGiftSub(data) {
+ if (!showTwitchSubs)
+ return;
+
+ const username = data.user.name;
+ const subTier = data.subTier;
+ const recipient = data.recipient.name;
+ const fromCommunitySubGift = data.fromCommunitySubGift;
+
+ // Don't post alerts for gift bombs
+ if (fromCommunitySubGift)
+ return;
+
+ let message = `🎁 ${username} gifted a Tier ${subTier.charAt(0)} subscription to ${recipient}`;
+
+ ShowAlert(message, 'twitch');
+}
+
+async function TwitchGiftBomb(data) {
+ if (!showTwitchSubs)
+ return;
+
+ const username = data.displayName;
+ const gifts = data.gifts;
+ const subTier = data.subTier;
+
+ let message = `🎁 ${username} gifted ${gifts} Tier ${subTier} subs!`;
+
+ ShowAlert(message, 'twitch');
+}
+
+async function TwitchRaid(data) {
+ if (!showTwitchRaids)
+ return;
+
+ const username = data.from_broadcaster_user_login;
+ const viewers = data.viewers;
+
+ let message = `${username} is raiding with a party of ${viewers}`;
+
+ ShowAlert(message, 'twitch');
+}
+
+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 => {
+ messageList.removeChild(item);
+ });
+}
+
+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);
+ }
+}
+
+function YouTubeMessage(data) {
+ if (!showYouTubeMessages)
+ return;
+
+ // Don't post messages starting with "!"
+ if (data.message.startsWith("!") && excludeCommands)
+ return;
+
+ // Don't post messages from users from the ignore list
+ if (ignoreUserList.includes(data.user.name))
+ return;
+
+ // Get a reference to the template
+ const template = document.getElementById('messageTemplate');
+
+ // Create a new instance of the template
+ const instance = template.content.cloneNode(true);
+
+ // Get divs
+ const userInfoDiv = instance.querySelector("#userInfo");
+ const avatarDiv = instance.querySelector("#avatar");
+ const timestampDiv = instance.querySelector("#timestamp");
+ const platformDiv = instance.querySelector("#platform");
+ const badgeListDiv = instance.querySelector("#badgeList");
+ const usernameDiv = instance.querySelector("#username");
+ const messageDiv = instance.querySelector("#message");
+
+ // Set timestamp
+ if (showTimestamps) {
+ timestampDiv.classList.add("timestamp");
+ timestampDiv.innerText = GetCurrentTimeFormatted();
+ }
+
+ // Set the message data
+ if (showUsername) {
+ usernameDiv.innerText = data.user.name;
+ usernameDiv.style.color = "#f70000"; // YouTube users do not have colors, so just set it to red
+ }
+
+ if (showMessage) {
+ messageDiv.innerText = data.message;
+ }
+
+ // Render platform
+ if (showPlatform) {
+ const platformElements = `
`;
+ platformDiv.innerHTML = platformElements;
+ }
+
+ // Render badges
+ if (data.user.isOwner && showBadges) {
+ const badge = new Image();
+ badge.src = `icons/badges/youtube-broadcaster.svg`;
+ badge.style.filter = `invert(100%)`;
+ badge.style.opacity = 0.8;
+ badge.classList.add("badge");
+ badgeListDiv.appendChild(badge);
+ }
+
+ if (data.user.isModerator && showBadges) {
+ const badge = new Image();
+ badge.src = `icons/badges/youtube-moderator.svg`;
+ badge.style.filter = `invert(100%)`;
+ badge.style.opacity = 0.8;
+ badge.classList.add("badge");
+ badgeListDiv.appendChild(badge);
+ }
+
+ if (data.user.isSponsor && showBadges) {
+ const badge = new Image();
+ badge.src = `icons/badges/youtube-member.svg`;
+ badge.style.filter = `invert(100%)`;
+ badge.style.opacity = 0.8;
+ badge.classList.add("badge");
+ badgeListDiv.appendChild(badge);
+ }
+
+ if (data.user.isVerified && showBadges) {
+ const badge = new Image();
+ badge.src = `icons/badges/youtube-verified.svg`;
+ badge.style.filter = `invert(100%)`;
+ badge.style.opacity = 0.8;
+ badge.classList.add("badge");
+ badgeListDiv.appendChild(badge);
+ }
+
+ // Render emotes
+ for (i in data.emotes) {
+ const emoteElement = ``;
+ messageDiv.innerHTML = messageDiv.innerHTML.replace(data.emotes[i].name, emoteElement);
+ }
+
+ // Render avatars
+ if (showAvatar) {
+ const avatar = new Image();
+ avatar.src = data.user.profileImageUrl;
+ avatar.classList.add("avatar");
+ avatarDiv.appendChild(avatar);
+ }
+
+ // Hide the header if the same username sends a message twice in a row
+ const messageList = document.getElementById("messageList");
+ if (messageList.children.length > 0) {
+ const lastPlatform = messageList.lastChild.dataset.platform;
+ const lastUserId = messageList.lastChild.dataset.userId;
+ if (lastPlatform == "youtube" && lastUserId == data.user.id)
+ userInfoDiv.style.display = "none";
+ }
+
+ AddMessageItem(instance, data.eventId, 'youtube', data.user.id);
+}
+
+function YouTubeSuperChat(data) {
+ if (!showYouTubeSuperChats)
+ return;
+
+ let message = `🪙 ${data.user.name} sent a Super Chat (${data.amount})`;
+
+ ShowAlert(message, 'youtube');
+}
+
+function YouTubeSuperSticker(data) {
+ if (!showYouTubeSuperStickers)
+ return;
+
+ let message = `${data.user.name} sent a Super Sticker (${data.amount})`;
+
+ ShowAlert(message, 'youtube');
+}
+
+function YouTubeNewSponsor(data) {
+ if (!showYouTubeMemberships)
+ return;
+
+ // Set message text
+ let message = `⭐ New ${data.levelName} • Welcome ${data.user.name}!`;
+
+ ShowAlert(message, 'youtube');
+}
+
+function YouTubeGiftMembershipReceived(data) {
+ if (!showYouTubeMemberships)
+ return;
+
+ let message = `🎁 ${data.gifter.name} gifted a membership to ${data.user.name} (${data.tier})!`;
+
+ ShowAlert(message, 'youtube');
+}
+
+function StreamlabsDonation(data) {
+ if (!showStreamlabsDonations)
+ return;
+
+ const donater = data.from;
+ const formattedAmount = data.formattedAmount;
+ const currency = data.currency;
+
+ let message = `🪙 ${donater} donated ${currency}${formattedAmount}`;
+
+ ShowAlert(message, 'streamlabs');
+}
+
+function StreamElementsTip(data) {
+ if (!showStreamElementsTips)
+ return;
+
+ const donater = data.username;
+ const formattedAmount = `$${data.amount}`;
+ const currency = data.currency;
+
+ let message = `🪙 ${donater} donated ${currency}${formattedAmount}`;
+
+ ShowAlert(message, 'streamelements');
+}
+
+
+
+//////////////////////
+// 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) {
+ const urlParams = new URLSearchParams(window.location.search);
+ const paramValue = urlParams.get(paramName);
+
+ if (paramValue === null) {
+ return null; // or undefined, or a default value, depending on your needs
+ }
+
+ 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 GetCurrentTimeFormatted() {
+ const now = new Date();
+ let hours = now.getHours();
+ const minutes = String(now.getMinutes()).padStart(2, '0');
+ 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;
+ }
+}
+
+async function GetPronouns(platform, username) {
+ const response = await client.getUserPronouns(platform, username);
+ const userFound = response.pronoun.userFound;
+ const pronouns = `${response.pronoun.pronounSubject}/${response.pronoun.pronounObject}`;
+
+ if (userFound)
+ return `${response.pronoun.pronounSubject}/${response.pronoun.pronounObject}`;
+ else
+ return '';
+}
+
+function AddMessageItem(element, elementID, platform, userId) {
+ // Calculate the height of the div before inserting
+ const tempDiv = document.getElementById('IPutThisHereSoICanCalculateHowBigEachMessageIsSupposedToBeBeforeIAddItToTheMessageList');
+ tempDiv.appendChild(element);
+
+ setTimeout(function () {
+ const calculatedWidth = tempDiv.offsetWidth + "px";
+ //console.log(calculatedWidth);
+
+ // Create a new line item to add to the message list later
+ var lineItem = document.createElement('li');
+ lineItem.id = elementID;
+ lineItem.dataset.platform = platform;
+ lineItem.dataset.userId = userId;
+
+ // Move the element from the temp div to the new line item
+ while (tempDiv.firstChild) {
+ lineItem.appendChild(tempDiv.firstChild);
+ }
+
+ // Add the line item to the list and animate it
+ // We need to manually set the height as straight CSS can't animate on "height: auto"
+ messageList.appendChild(lineItem);
+ setTimeout(function () {
+ lineItem.className = lineItem.className + " show";
+ lineItem.style.width = calculatedWidth;
+ }, 10);
+
+ // Remove old messages that have gone off screen to save memory
+ while (messageList.clientWidth > 10 * window.innerWidth) {
+ messageList.removeChild(messageList.firstChild);
+ }
+
+ tempDiv.innerHTML = '';
+
+ if (hideAfter > 0)
+ {
+ setTimeout(function () {
+ lineItem.style.opacity = 0;
+ setTimeout(function() {
+ messageList.removeChild(lineItem);
+ }, 1000);
+ }, hideAfter * 1000);
+ }
+
+ }, 200);
+}
+
+// I used Gemini for this shit so if it doesn't work, blame Google
+function FindFirstImageUrl(jsonObject) {
+ if (typeof jsonObject !== 'object' || jsonObject === null) {
+ return null; // Handle invalid input
+ }
+
+ function iterate(obj) {
+ if (Array.isArray(obj)) {
+ for (const item of obj) {
+ const result = iterate(item);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ for (const key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ if (key === 'imageUrl') {
+ return obj[key]; // Found it! Return the value.
+ }
+
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
+ const result = iterate(obj[key]); // Recursive call for nested objects
+ if (result) {
+ return result; // Propagate the found value
+ }
+ }
+ }
+ }
+ return null; // Key not found in this level
+ }
+
+ return iterate(jsonObject);
+}
+
+function ShowAlert(message, background = null, duration = animationDuration) {
+
+ // Check if the widget is in the middle of an animation
+ // If any alerts are requested while the animation is playing, it should be added to the alert queue
+ if (widgetLocked) {
+ console.debug("Animation is progress, added alert to queue");
+ let data = { message: message, background: background, duration: duration };
+ alertQueue.push(data);
+ return;
+ }
+
+ // Get divs
+ const messageListDiv = document.querySelector("#messageList");
+ const alertBoxDiv = document.querySelector("#alertBox");
+
+ // Set the message text
+ alertBoxDiv.innerHTML = message;
+
+ // Set the background
+ alertBoxDiv.classList.add(background);
+
+ // Start the animation
+ widgetLocked = true;
+ messageListDiv.style.animation = 'hideAlertBox 0.5s ease-in-out forwards';
+ alertBoxDiv.style.animation = 'showAlertBox 0.5s ease-in-out forwards';
+
+ // To stop the animation (remove the animation property):
+ setTimeout(() => {
+ messageListDiv.style.animation = 'showAlertBox 0.5s ease-in-out forwards';
+ alertBoxDiv.style.animation = 'hideAlertBox 0.5s ease-in-out forwards';
+ setTimeout(() => {
+ alertBoxDiv.classList = '';
+ widgetLocked = false;
+ if (alertQueue.length > 0) {
+ console.debug("Pulling next alert from the queue");
+ let data = alertQueue.shift();
+ ShowAlert(data.message, data.background, data.duration);
+ }
+ }, 500);
+ }, duration); // Remove after 5 seconds
+}
+
+
+
+///////////////////////////////////
+// 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;
+ }
+}
+
+// let data = {
+// "isAnonymous": false,
+// "gifts": 10,
+// "totalGifts": 0,
+// "subTier": 1, /* 0 - Prime, 1 - Tier 1, 2 - Tier 2, 3 - Tier 3 */
+// "userName": "
zT?pp3e5W}oBHa>hGDH&|IL{Bs(f@}Z;@@j=wf~%ZBSLd_
Q-%<~M%IajrUmn;0A5D{si2wiq
literal 0
HcmV?d00001
diff --git a/multichat-overlay/index.html b/multichat-overlay/index.html
new file mode 100644
index 0000000..2195660
--- /dev/null
+++ b/multichat-overlay/index.html
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
+ platformDiv.innerHTML = platformElements;
+ }
+
+ // 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 = ``;
+ messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${data.emotes[i].name}\\b`), emoteElement);
+ }
+
+ // Render cheermotes
+ for (i in data.cheerEmotes) {
+ const bits = data.cheerEmotes[i].bits;
+ const imageUrl = data.cheerEmotes[i].imageUrl;
+ const name = data.cheerEmotes[i].name;
+ const cheerEmoteElement = `
`;
+ const bitsElements = `${bits}`
+ messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements);
+ }
+
+ // Render avatars
+ if (showAvatar) {
+ const username = data.message.username;
+ const avatarURL = await GetAvatar(username);
+ const avatar = new Image();
+ avatar.src = avatarURL;
+ avatar.classList.add("avatar");
+ avatarDiv.appendChild(avatar);
+ }
+
+ // Hide the header if the same username sends a message twice in a row
+ // EXCEPT when the scroll direction is set to reverse (scrollDirection == 2)
+ const messageList = document.getElementById("messageList");
+ if (messageList.children.length > 0 && scrollDirection != 2) {
+ const lastPlatform = messageList.lastChild.dataset.platform;
+ const lastUserId = messageList.lastChild.dataset.userId;
+ if (lastPlatform == "twitch" && lastUserId == data.user.id)
+ userInfoDiv.style.display = "none";
+ }
+
+ // Embed image
+ if (IsThisUserAllowedToPostImagesOrNotReturnTrueIfTheyCanReturnFalseIfTheyCannot(imageEmbedPermissionLevel, data, 'twitch') && IsImageUrl(message)) {
+ const image = new Image();
+
+ image.onload = function () {
+ image.style.padding = "20px 0px";
+ image.style.width = "100%";
+ messageDiv.innerHTML = '';
+ messageDiv.appendChild(image);
+
+ AddMessageItem(instance, data.message.msgId, 'twitch', data.user.id);
+ };
+
+ const urlObj = new URL(message);
+ urlObj.search = '';
+ urlObj.hash = '';
+
+ image.src = "https://external-content.duckduckgo.com/iu/?u=" + urlObj.toString();
+ }
+ else {
+ AddMessageItem(instance, data.message.msgId, 'twitch', data.user.id);
+ }
+}
+
+async function TwitchAutomaticRewardRedemption(data) {
+ // Get a reference to the template
+ const template = document.getElementById('messageTemplate');
+
+ // Create a new instance of the template
+ const instance = template.content.cloneNode(true);
+
+ // Get divs
+ const messageContainerDiv = instance.querySelector("#messageContainer");
+ const firstMessageDiv = instance.querySelector("#firstMessage");
+ const replyDiv = instance.querySelector("#reply");
+ const replyUserDiv = instance.querySelector("#replyUser");
+ const replyMsgDiv = instance.querySelector("#replyMsg");
+ const userInfoDiv = instance.querySelector("#userInfo");
+ const avatarDiv = instance.querySelector("#avatar");
+ const timestampDiv = instance.querySelector("#timestamp");
+ const platformDiv = instance.querySelector("#platform");
+ const badgeListDiv = instance.querySelector("#badgeList");
+ const usernameDiv = instance.querySelector("#username");
+ const messageDiv = instance.querySelector("#message");
+
+ if (data.reward_type != 'gigantify_an_emote')
+ return;
+
+ userInfoDiv.style.display = "none";
+
+ // Show the gigantified emote
+ const gigaEmote = data.gigantified_emote.imageUrl;
+ const image = new Image();
+ image.src = gigaEmote;
+ image.style.padding = "20px 0px";
+ image.style.width = "50%";
+
+ image.onload = function () {
+ messageDiv.innerHTML = '';
+ messageDiv.appendChild(image);
+ }
+
+ AddMessageItem(instance, data.id);
+}
+
+async function TwitchAnnouncement(data) {
+ if (!showTwitchAnnouncements)
+ return;
+
+ // Get a reference to the template
+ const template = document.getElementById('cardTemplate');
+
+ // Create a new instance of the template
+ const instance = template.content.cloneNode(true);
+
+ // Get divs
+ const cardDiv = instance.querySelector("#card");
+ const headerDiv = instance.querySelector("#header");
+ const avatarDiv = instance.querySelector("#avatar");
+ const timestampDiv = instance.querySelector("#timestamp");
+ const iconDiv = instance.querySelector("#icon");
+ const titleDiv = instance.querySelector("#title");
+ const contentDiv = instance.querySelector("#contentDiv");
+
+ // Set the card background colors
+ switch (data.announcementColor) {
+ case "BLUE":
+ cardDiv.classList.add('announcementBlue');
+ break;
+ case "GREEN":
+ cardDiv.classList.add('announcementGreen');
+ break;
+ case "ORANGE":
+ cardDiv.classList.add('announcementOrange');
+ break;
+ case "PURPLE":
+ cardDiv.classList.add('announcementPurple');
+ break;
+ }
+
+ // Set the card header
+ iconDiv.innerText = "📢";
+ titleDiv.innerText = "Announcement";
+
+ // Get a reference to the message template
+ const contentTemplate = document.getElementById('messageTemplate');
+
+ // Create a new instance of the template
+ const content = contentTemplate.content.cloneNode(true);
+
+ // Set timestamp
+ if (showTimestamps) {
+ content.querySelector("#timestamp").classList.add("timestamp");
+ content.querySelector("#timestamp").innerText = GetCurrentTimeFormatted();
+ }
+ content.querySelector("#username").innerText = data.user.name;
+ content.querySelector("#username").style.color = data.user.color;
+ content.querySelector("#message").innerText = data.text;
+
+ // Remove the line break
+ content.querySelector("#colon-separator").style.display = `inline`;
+ content.querySelector("#line-space").style.display = `none`;
+
+ // Render platform
+ content.querySelector("#platform").style.display = `none`;
+
+ // Render badges
+ content.querySelector("#badgeList").innerHTML = "";
+ for (i in data.user.badges) {
+ const badge = new Image();
+ badge.src = data.user.badges[i].imageUrl;
+ badge.classList.add("badge");
+ content.querySelector("#badgeList").appendChild(badge);
+ }
+
+ // Set pronouns
+ const pronouns = await GetPronouns('twitch', data.user.login);
+ if (pronouns) {
+ content.querySelector("#pronouns").classList.add("pronouns");
+ content.querySelector("#pronouns").innerText = pronouns;
+ }
+
+ // Render emotes
+ for (i in data.parts) {
+ if (data.parts[i].type == `emote`) {
+ const emoteElement = `
`;
+ content.querySelector("#message").innerHTML = content.querySelector("#message").innerHTML.replace(new RegExp(`\\b${data.parts[i].text}\\b`), emoteElement);
+ }
+ }
+
+ // Insert the modified template instance into the DOM
+ instance.querySelector("#content").appendChild(content);
+
+ AddMessageItem(instance, data.messageId);
+}
+
+async function TwitchSub(data) {
+ if (!showTwitchSubs)
+ return;
+
+ // Get a reference to the template
+ const template = document.getElementById('cardTemplate');
+
+ // Create a new instance of the template
+ const instance = template.content.cloneNode(true);
+
+ // Get divs
+ const cardDiv = instance.querySelector("#card");
+ const headerDiv = instance.querySelector("#header");
+ const avatarDiv = instance.querySelector("#avatar");
+ const iconDiv = instance.querySelector("#icon");
+ const titleDiv = instance.querySelector("#title");
+ const contentDiv = instance.querySelector("#contentDiv");
+
+ // Set the card background colors
+ cardDiv.classList.add('twitch');
+
+ // Set the card header
+ for (i in data.user.badges) {
+ if (data.user.badges[i].name == "subscriber") {
+ const badge = new Image();
+ badge.src = data.user.badges[i].imageUrl;
+ badge.classList.add("badge");
+ iconDiv.appendChild(badge);
+ }
+ }
+
+ // Set the text
+ const username = data.user.name;
+ const subTier = data.sub_tier;
+ const isPrime = data.is_prime;
+
+ if (!isPrime)
+ titleDiv.innerText = `${username} subscribed with Tier ${subTier.charAt(0)}`;
+ else
+ titleDiv.innerText = `${username} used their Prime Sub`;
+
+ AddMessageItem(instance, data.messageId);
+}
+
+async function TwitchResub(data) {
+ if (!showTwitchSubs)
+ return;
+
+ // Get a reference to the template
+ const template = document.getElementById('cardTemplate');
+
+ // Create a new instance of the template
+ const instance = template.content.cloneNode(true);
+
+ // Get divs
+ const cardDiv = instance.querySelector("#card");
+ const headerDiv = instance.querySelector("#header");
+ const avatarDiv = instance.querySelector("#avatar");
+ const iconDiv = instance.querySelector("#icon");
+ const titleDiv = instance.querySelector("#title");
+ const contentDiv = instance.querySelector("#content");
+
+ // Set the card background colors
+ cardDiv.classList.add('twitch');
+
+ // Set the card header
+ for (i in data.user.badges) {
+ if (data.user.badges[i].name == "subscriber") {
+ const badge = new Image();
+ badge.src = data.user.badges[i].imageUrl;
+ badge.classList.add("badge");
+ iconDiv.appendChild(badge);
+ }
+ }
+
+ // Set the text
+ const username = data.user.name;
+ const subTier = data.subTier;
+ const isPrime = data.isPrime;
+ const cumulativeMonths = data.cumulativeMonths;
+ const message = data.text;
+
+ if (!isPrime)
+ titleDiv.innerText = `${username} resubscribed with Tier ${subTier.charAt(0)} (${cumulativeMonths} months)`;
+ else
+ titleDiv.innerText = `${username} used their Prime Sub (${cumulativeMonths} months)`;
+ contentDiv.innerText = `${message}`;
+
+ AddMessageItem(instance, data.messageId);
+}
+
+async function TwitchGiftSub(data) {
+ if (!showTwitchSubs)
+ return;
+
+ // Get a reference to the template
+ const template = document.getElementById('cardTemplate');
+
+ // Create a new instance of the template
+ const instance = template.content.cloneNode(true);
+
+ // Get divs
+ const cardDiv = instance.querySelector("#card");
+ const headerDiv = instance.querySelector("#header");
+ const avatarDiv = instance.querySelector("#avatar");
+ const iconDiv = instance.querySelector("#icon");
+ const titleDiv = instance.querySelector("#title");
+ const contentDiv = instance.querySelector("#content");
+
+ // Set the card background colors
+ cardDiv.classList.add('twitch');
+
+ // Set the card header
+ for (i in data.user.badges) {
+ if (data.user.badges[i].name == "subscriber") {
+ const badge = new Image();
+ badge.src = data.user.badges[i].imageUrl;
+ badge.classList.add("badge");
+ iconDiv.appendChild(badge);
+ }
+ }
+
+ // Set the text
+ const username = data.user.name;
+ const subTier = data.subTier;
+ const recipient = data.recipient.name;
+ const cumlativeTotal = data.cumlativeTotal;
+
+ titleDiv.innerText = `${username} gifted a Tier ${subTier.charAt(0)} subscription to ${recipient}`;
+ if (cumlativeTotal > 0)
+ contentDiv.innerText = `They've gifted ${cumlativeTotal} subs in total!`;
+
+ AddMessageItem(instance, data.messageId);
+}
+
+async function TwitchRaid(data) {
+ if (!showTwitchRaids)
+ return;
+
+ // Get a reference to the template
+ const template = document.getElementById('cardTemplate');
+
+ // Create a new instance of the template
+ const instance = template.content.cloneNode(true);
+
+ // Get divs
+ const cardDiv = instance.querySelector("#card");
+ const headerDiv = instance.querySelector("#header");
+ const avatarDiv = instance.querySelector("#avatar");
+ const iconDiv = instance.querySelector("#icon");
+ const titleDiv = instance.querySelector("#title");
+ const contentDiv = instance.querySelector("#content");
+
+ // Set the card background colors
+ cardDiv.classList.add('twitch');
+
+ if (showAvatar) {
+ // Render avatars
+ const username = data.from_broadcaster_user_login;
+ const avatarURL = await GetAvatar(username);
+ const avatar = new Image();
+ avatar.src = avatarURL;
+ avatar.classList.add("avatar");
+ avatarDiv.appendChild(avatar);
+ }
+
+
+ // Set the text
+ const username = data.from_broadcaster_user_login;
+ const viewers = data.viewers;
+
+ titleDiv.innerText = `${username} is raiding`;
+ contentDiv.innerText = `with a party of ${viewers}`;
+
+ AddMessageItem(instance, data.messageId);
+}
+
+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);
+ }, 1000);
+ });
+}
+
+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);
+ }
+}
+
+function YouTubeMessage(data) {
+ if (!showYouTubeMessages)
+ return;
+
+ // Don't post messages starting with "!"
+ if (data.message.startsWith("!") && excludeCommands)
+ return;
+
+ // Don't post messages from users from the ignore list
+ if (ignoreUserList.includes(data.user.name))
+ return;
+
+ // Get a reference to the template
+ const template = document.getElementById('messageTemplate');
+
+ // Create a new instance of the template
+ const instance = template.content.cloneNode(true);
+
+ // Get divs
+ const userInfoDiv = instance.querySelector("#userInfo");
+ const avatarDiv = instance.querySelector("#avatar");
+ const timestampDiv = instance.querySelector("#timestamp");
+ const platformDiv = instance.querySelector("#platform");
+ const badgeListDiv = instance.querySelector("#badgeList");
+ const usernameDiv = instance.querySelector("#username");
+ const messageDiv = instance.querySelector("#message");
+
+ // Set timestamp
+ if (showTimestamps) {
+ timestampDiv.classList.add("timestamp");
+ timestampDiv.innerText = GetCurrentTimeFormatted();
+ }
+
+ // Set the message data
+ if (showUsername) {
+ usernameDiv.innerText = data.user.name;
+ usernameDiv.style.color = "#f70000"; // YouTube users do not have colors, so just set it to red
+ }
+
+ if (showMessage) {
+ messageDiv.innerText = data.message;
+ }
+
+
+ // Render platform
+ if (showPlatform) {
+ const platformElements = `
`;
+ platformDiv.innerHTML = platformElements;
+ }
+
+ // Render badges
+ if (data.user.isOwner && showBadges) {
+ const badge = new Image();
+ badge.src = `icons/badges/youtube-broadcaster.svg`;
+ badge.style.filter = `invert(100%)`;
+ badge.style.opacity = 0.8;
+ badge.classList.add("badge");
+ badgeListDiv.appendChild(badge);
+ }
+
+ if (data.user.isModerator && showBadges) {
+ const badge = new Image();
+ badge.src = `icons/badges/youtube-moderator.svg`;
+ badge.style.filter = `invert(100%)`;
+ badge.style.opacity = 0.8;
+ badge.classList.add("badge");
+ badgeListDiv.appendChild(badge);
+ }
+
+ if (data.user.isSponsor && showBadges) {
+ const badge = new Image();
+ badge.src = `icons/badges/youtube-member.svg`;
+ badge.style.filter = `invert(100%)`;
+ badge.style.opacity = 0.8;
+ badge.classList.add("badge");
+ badgeListDiv.appendChild(badge);
+ }
+
+ if (data.user.isVerified && showBadges) {
+ const badge = new Image();
+ badge.src = `icons/badges/youtube-verified.svg`;
+ badge.style.filter = `invert(100%)`;
+ badge.style.opacity = 0.8;
+ badge.classList.add("badge");
+ badgeListDiv.appendChild(badge);
+ }
+
+ // Render emotes
+ for (i in data.emotes) {
+ const emoteElement = ``;
+ // messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${data.emotes[i].name}\\b`), emoteElement);
+ messageDiv.innerHTML = messageDiv.innerHTML.replace(data.emotes[i].name, emoteElement);
+ }
+
+ // Render avatars
+ if (showAvatar) {
+ const avatar = new Image();
+ avatar.src = data.user.profileImageUrl;
+ avatar.classList.add("avatar");
+ avatarDiv.appendChild(avatar);
+ }
+
+
+ // Hide the header if the same username sends a message twice in a row
+ // EXCEPT when the scroll direction is set to reverse (scrollDirection == 2)
+ const messageList = document.getElementById("messageList");
+ if (messageList.children.length > 0 && scrollDirection != 2) {
+ const lastPlatform = messageList.lastChild.dataset.platform;
+ const lastUserId = messageList.lastChild.dataset.userId;
+ if (lastPlatform == "youtube" && lastUserId == data.user.id)
+ userInfoDiv.style.display = "none";
+ }
+
+ // Embed image
+ const message = data.message;
+ if (IsThisUserAllowedToPostImagesOrNotReturnTrueIfTheyCanReturnFalseIfTheyCannot(imageEmbedPermissionLevel, data, 'youtube') && IsImageUrl(message)) {
+ const image = new Image();
+
+ image.onload = function () {
+ image.style.padding = "20px 0px";
+ image.style.width = "100%";
+ messageDiv.innerHTML = '';
+ messageDiv.appendChild(image);
+
+ AddMessageItem(instance, data.message.msgId, 'youtube', data.user.id);
+ };
+
+ const urlObj = new URL(message);
+ urlObj.search = '';
+ urlObj.hash = '';
+
+ image.src = "https://external-content.duckduckgo.com/iu/?u=" + urlObj.toString();
+ }
+ else {
+ AddMessageItem(instance, data.eventId, 'youtube', data.user.id);
+ }
+}
+
+function YouTubeSuperChat(data) {
+ if (!showYouTubeSuperChats)
+ return;
+
+ // Get a reference to the template
+ const template = document.getElementById('cardTemplate');
+
+ // Create a new instance of the template
+ const instance = template.content.cloneNode(true);
+
+ // Get divs
+ const cardDiv = instance.querySelector("#card");
+ const headerDiv = instance.querySelector("#header");
+ const avatarDiv = instance.querySelector("#avatar");
+ const iconDiv = instance.querySelector("#icon");
+ const titleDiv = instance.querySelector("#title");
+ const contentDiv = instance.querySelector("#content");
+
+ // Set the card background colors
+ cardDiv.classList.add('youtube');
+
+ // Set message text
+ titleDiv.innerText = `🪙 ${data.user.name} sent a Super Chat (${data.amount})`;
+ contentDiv.innerText = `${data.message}!`;
+
+ AddMessageItem(instance, data.eventId);
+}
+
+function YouTubeSuperSticker(data) {
+ if (!showYouTubeSuperStickers)
+ return;
+
+ // Get a reference to the template
+ const template = document.getElementById('cardTemplate');
+
+ // Create a new instance of the template
+ const instance = template.content.cloneNode(true);
+
+ // Get divs
+ const cardDiv = instance.querySelector("#card");
+ const headerDiv = instance.querySelector("#header");
+ const avatarDiv = instance.querySelector("#avatar");
+ const iconDiv = instance.querySelector("#icon");
+ const titleDiv = instance.querySelector("#title");
+ const contentDiv = instance.querySelector("#content");
+
+ // Set the card background colors
+ cardDiv.classList.add('youtube');
+
+ const stickerTemplate = document.getElementById('stickerTemplate');
+
+ // Create a new instance of the template
+ const stickerInstance = stickerTemplate.content.cloneNode(true);
+
+ // Render sticker
+ stickerInstance.querySelector("#stickerImg").src = FindFirstImageUrl(data);
+ stickerInstance.querySelector("#stickerLabel").innerText = `${data.user.name} sent a Super Sticker (${data.amount})`;
+
+ contentDiv.appendChild(stickerInstance);
+
+ AddMessageItem(instance, data.eventId);
+}
+
+function YouTubeNewSponsor(data) {
+ if (!showYouTubeMemberships)
+ return;
+
+ // Get a reference to the template
+ const template = document.getElementById('cardTemplate');
+
+ // Create a new instance of the template
+ const instance = template.content.cloneNode(true);
+
+ // Get divs
+ const cardDiv = instance.querySelector("#card");
+ const headerDiv = instance.querySelector("#header");
+ const avatarDiv = instance.querySelector("#avatar");
+ const iconDiv = instance.querySelector("#icon");
+ const titleDiv = instance.querySelector("#title");
+ const contentDiv = instance.querySelector("#content");
+
+ // Set the card background colors
+ cardDiv.classList.add('youtube');
+
+ // Set message text
+ titleDiv.innerText = `⭐ New ${data.levelName}`;
+ contentDiv.innerText = `Welcome ${data.user.name}!`;
+
+ AddMessageItem(instance, data.eventId);
+}
+
+function YouTubeGiftMembershipReceived(data) {
+ if (!showYouTubeMemberships)
+ return;
+
+ // Get a reference to the template
+ const template = document.getElementById('cardTemplate');
+
+ // Create a new instance of the template
+ const instance = template.content.cloneNode(true);
+
+ // Get divs
+ const cardDiv = instance.querySelector("#card");
+ const headerDiv = instance.querySelector("#header");
+ const avatarDiv = instance.querySelector("#avatar");
+ const iconDiv = instance.querySelector("#icon");
+ const titleDiv = instance.querySelector("#title");
+ const contentDiv = instance.querySelector("#content");
+
+ // Set the card background colors
+ cardDiv.classList.add('youtube');
+
+ // Set message text
+ titleDiv.innerText = `🎁 ${data.gifter.name} gifted a membership`;
+ contentDiv.innerText = `to ${data.user.name} (${data.tier})!`;
+
+ AddMessageItem(instance, data.eventId);
+}
+
+async function StreamlabsDonation(data) {
+ if (!showStreamlabsDonations)
+ return;
+
+ // Get a reference to the template
+ const template = document.getElementById('cardTemplate');
+
+ // Create a new instance of the template
+ const instance = template.content.cloneNode(true);
+
+ // Get divs
+ const cardDiv = instance.querySelector("#card");
+ const headerDiv = instance.querySelector("#header");
+ const avatarDiv = instance.querySelector("#avatar");
+ const iconDiv = instance.querySelector("#icon");
+ const titleDiv = instance.querySelector("#title");
+ const contentDiv = instance.querySelector("#content");
+
+ // Set the card background colors
+ cardDiv.classList.add('streamlabs');
+
+ // Set the text
+ const donater = data.from;
+ const formattedAmount = data.formattedAmount;
+ const currency = data.currency;
+ const message = data.message;
+
+ titleDiv.innerText = `🪙 ${donater} donated ${currency}${formattedAmount}`;
+ contentDiv.innerText = `${message}`;
+
+ AddMessageItem(instance);
+}
+
+async function StreamElementsTip(data) {
+ if (!showStreamElementsTips)
+ return;
+
+ // Get a reference to the template
+ const template = document.getElementById('cardTemplate');
+
+ // Create a new instance of the template
+ const instance = template.content.cloneNode(true);
+
+ // Get divs
+ const cardDiv = instance.querySelector("#card");
+ const headerDiv = instance.querySelector("#header");
+ const avatarDiv = instance.querySelector("#avatar");
+ const iconDiv = instance.querySelector("#icon");
+ const titleDiv = instance.querySelector("#title");
+ const contentDiv = instance.querySelector("#content");
+
+ // Set the card background colors
+ cardDiv.classList.add('streamelements');
+
+ // Set the text
+ const donater = data.username;
+ const formattedAmount = `$${data.amount}`;
+ const currency = data.currency;
+ const message = data.message;
+
+ titleDiv.innerText = `🪙 ${donater} donated ${currency}${formattedAmount}`;
+ contentDiv.innerText = `${message}`;
+
+ AddMessageItem(instance, data.id);
+}
+
+
+
+//////////////////////
+// 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) {
+ const urlParams = new URLSearchParams(window.location.search);
+ const paramValue = urlParams.get(paramName);
+
+ if (paramValue === null) {
+ return null; // or undefined, or a default value, depending on your needs
+ }
+
+ 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 GetCurrentTimeFormatted() {
+ const now = new Date();
+ let hours = now.getHours();
+ const minutes = String(now.getMinutes()).padStart(2, '0');
+ 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;
+ }
+}
+
+async function GetPronouns(platform, username) {
+ const response = await client.getUserPronouns(platform, username);
+ const userFound = response.pronoun.userFound;
+ const pronouns = `${response.pronoun.pronounSubject}/${response.pronoun.pronounObject}`;
+
+ if (userFound)
+ return `${response.pronoun.pronounSubject}/${response.pronoun.pronounObject}`;
+ else
+ return '';
+}
+
+// function IsImageUrl(url) {
+// return url.match(/^http.*\.(jpeg|jpg|gif|png)$/) != null;
+// }
+
+function IsImageUrl(url) {
+ try {
+ const { pathname } = new URL(url);
+ // Only check the pathname since query parameters are not included in it.
+ return /\.(png|jpe?g|webp|gif)$/i.test(pathname);
+ } catch (error) {
+ // Return false if the URL is invalid.
+ return false;
+ }
+}
+
+function AddMessageItem(element, elementID, platform, userId) {
+ // Calculate the height of the div before inserting
+ const tempDiv = document.getElementById('IPutThisHereSoICanCalculateHowBigEachMessageIsSupposedToBeBeforeIAddItToTheMessageList');
+ tempDiv.appendChild(element);
+
+ setTimeout(function () {
+ const calculatedHeight = tempDiv.clientHeight + "px";
+
+ // Create a new line item to add to the message list later
+ var lineItem = document.createElement('li');
+ lineItem.id = elementID;
+ lineItem.dataset.platform = platform;
+ lineItem.dataset.userId = userId;
+
+ // Set scroll direction
+ if (scrollDirection == 2)
+ lineItem.classList.add('reverseLineItemDirection');
+
+ // Move the element from the temp div to the new line item
+ while (tempDiv.firstChild) {
+ lineItem.appendChild(tempDiv.firstChild);
+ }
+
+ // Add the line item to the list and animate it
+ // We need to manually set the height as straight CSS can't animate on "height: auto"
+ messageList.appendChild(lineItem);
+ setTimeout(function () {
+ lineItem.className = lineItem.className + " show";
+ lineItem.style.height = calculatedHeight;
+ }, 10);
+
+ // Remove old messages that have gone off screen to save memory
+ while (messageList.clientHeight > 5 * window.innerHeight) {
+ messageList.removeChild(messageList.firstChild);
+ }
+
+ tempDiv.innerHTML = '';
+
+ if (hideAfter > 0)
+ {
+ setTimeout(function () {
+ lineItem.style.opacity = 0;
+ setTimeout(function() {
+ messageList.removeChild(lineItem);
+ }, 1000);
+ }, hideAfter * 1000);
+ }
+
+ }, 100);
+}
+
+// I used Gemini for this shit so if it doesn't work, blame Google
+function FindFirstImageUrl(jsonObject) {
+ if (typeof jsonObject !== 'object' || jsonObject === null) {
+ return null; // Handle invalid input
+ }
+
+ function iterate(obj) {
+ if (Array.isArray(obj)) {
+ for (const item of obj) {
+ const result = iterate(item);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ for (const key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ if (key === 'imageUrl') {
+ return obj[key]; // Found it! Return the value.
+ }
+
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
+ const result = iterate(obj[key]); // Recursive call for nested objects
+ if (result) {
+ return result; // Propagate the found value
+ }
+ }
+ }
+ }
+ return null; // Key not found in this level
+ }
+
+ return iterate(jsonObject);
+}
+
+function IsThisUserAllowedToPostImagesOrNotReturnTrueIfTheyCanReturnFalseIfTheyCannot(targetPermissions, data, platform) {
+ return GetPermissionLevel(data, platform) >= targetPermissions;
+}
+
+function GetPermissionLevel(data, platform) {
+ switch (platform) {
+ case 'twitch':
+ if (data.message.role >= 4)
+ return 40;
+ else if (data.message.role >= 3)
+ return 30;
+ else if (data.message.role >= 2)
+ return 20;
+ else if (data.message.role >= 2 || data.message.subscriber)
+ return 15;
+ else
+ return 10;
+ case 'youtube':
+ if (data.user.isOwner)
+ return 40;
+ else if (data.user.isModerator)
+ return 30;
+ else if (data.user.isSponsor)
+ return 15;
+ else
+ return 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;
+ }
+}
+
+// let data = `{
+// "bits": 1,
+// "cheerEmotes": [
+// {
+// "bits": 1,
+// "color": "#979797",
+// "type": "CheerEmote",
+// "name": "Cheer",
+// "startIndex": 0,
+// "endIndex": 5,
+// "imageUrl": "https://d3aqoihi2n8ty8.cloudfront.net/actions/cheer/dark/animated/1/4.gif"
+// }
+// ],
+// "message": {
+// "internal": false,
+// "msgId": "ac304b97-35fd-4623-9ab3-2e68f24e770a",
+// "userId": "24586202",
+// "username": "cookievscookie",
+// "role": 1,
+// "subscriber": true,
+// "subscriptionTier": "1000",
+// "displayName": "CookieVsCookie",
+// "color": "#DEC27A",
+// "channel": "nutty",
+// "message": "Cheer1 wasting all of my money 1 bit at time",
+// "isHighlighted": false,
+// "isMe": false,
+// "isCustomReward": false,
+// "isAnonymous": false,
+// "isReply": false,
+// "bits": 1,
+// "firstMessage": false,
+// "returningChatter": false,
+// "hasBits": true,
+// "emotes": [],
+// "cheerEmotes": [
+// {
+// "bits": 1,
+// "color": "#979797",
+// "type": "CheerEmote",
+// "name": "Cheer",
+// "startIndex": 0,
+// "endIndex": 5,
+// "imageUrl": "https://d3aqoihi2n8ty8.cloudfront.net/actions/cheer/dark/animated/1/4.gif"
+// }
+// ],
+// "badges": [
+// {
+// "name": "subscriber",
+// "version": "3",
+// "imageUrl": "https://static-cdn.jtvnw.net/badges/v1/c2d0acb4-a706-43a1-9d2b-0458cde2ba93/3",
+// "info": "4"
+// },
+// {
+// "name": "share-the-love",
+// "version": "1",
+// "imageUrl": "https://static-cdn.jtvnw.net/badges/v1/2de71f4f-b152-4308-a426-127a4cf8003a/3",
+// "info": ""
+// }
+// ],
+// "monthsSubscribed": 4,
+// "isTest": false,
+// "sharedChat": false,
+// "sourceBadges": []
+// },
+// "user": {
+// "role": 1,
+// "badges": [
+// {
+// "name": "subscriber",
+// "version": "3",
+// "imageUrl": "https://static-cdn.jtvnw.net/badges/v1/c2d0acb4-a706-43a1-9d2b-0458cde2ba93/3",
+// "info": "4"
+// },
+// {
+// "name": "share-the-love",
+// "version": "1",
+// "imageUrl": "https://static-cdn.jtvnw.net/badges/v1/2de71f4f-b152-4308-a426-127a4cf8003a/3",
+// "info": ""
+// }
+// ],
+// "color": "#DEC27A",
+// "subscribed": true,
+// "subscriptionTier": "1000",
+// "monthsSubscribed": 4,
+// "id": "24586202",
+// "login": "cookievscookie",
+// "name": "CookieVsCookie",
+// "type": "twitch"
+// },
+// "messageId": "ac304b97-35fd-4623-9ab3-2e68f24e770a",
+// "meta": {
+// "internal": false,
+// "firstMessage": false,
+// "returningChatter": false,
+// "isHighlighted": false,
+// "isMe": false,
+// "isCustomReward": false,
+// "isTest": false
+// },
+// "anonymous": false,
+// "text": "Cheer1 wasting all of my money 1 bit at time",
+// "emotes": [],
+// "parts": [
+// {
+// "bits": 1,
+// "color": "#979797",
+// "imageUrl": "https://d3aqoihi2n8ty8.cloudfront.net/actions/cheer/dark/animated/1/4.gif",
+// "type": "cheer",
+// "text": "Cheer"
+// },
+// {
+// "type": "text",
+// "text": " wasting all of my money 1 bit at time"
+// }
+// ],
+// "isReply": false,
+// "isSharedChat": false,
+// "isTest": false
+// }`;
+
+// TwitchChatMessage(JSON.parse(data));
\ No newline at end of file
diff --git a/multichat-overlay/settings/index.html b/multichat-overlay/settings/index.html
new file mode 100644
index 0000000..c4eabbf
--- /dev/null
+++ b/multichat-overlay/settings/index.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
p@j$@^P9W2`A=1p~kedIKCOXwH!^I$@O8@$%bew
zyGS_yD(&YRH1
t&)^4`f!DnH3gs{UInVx!P$-
zPR-#?nK68gCSK@2+xKDm0T#MGw0vTry!ceG;W*y&$7sa1*ejgK()Y3bgGzef$65=0
z?O
0H9
z9$`ab7Ti>u-k=Svm=Z6h
Ao&2Bg~%On+^1x+DSQLhHz9R-KI0+iYo
zFZkpzFqr?;>;g3YAgqsceh6N!DWf73{Oduw{9X;p
zE15QRxfon#to4xsNcFvSq>h-n5W7k0#xJnFnHD1O>s@1Y6I#W~YAZyr98a)p5zJm?
z<^A!mz9vJ@{DGZhV1?cd8)*lIUJP~?DZ)9B8?N?+UgUkn+GzI?FRvf}63n}UWdzG~
z8J1_m9lLkN5_5;Pq=6i49Bd^!X4rCV$|*0m4|jO2f1lw2uvp?$Jrr!Axr(_Wp$@td
z-;>L?U5b5?#uW3E+r)vubGxm`LwL?{+o{yhf^RY#SXwRqA*)uO$(yvttnGVI3SG2E
z)HP&6zL)g
w>?XGdls
z$`+7?5nB>g1FZ53(Cge4