From fc63f166b51badd9b5eee6fd81129c11988d9957 Mon Sep 17 00:00:00 2001 From: nutty Date: Sun, 12 Apr 2026 02:04:57 +1000 Subject: [PATCH 01/11] Fixed Kick avatars --- .common/utils/helpers.js | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/.common/utils/helpers.js b/.common/utils/helpers.js index 4c4d0f1..05009eb 100644 --- a/.common/utils/helpers.js +++ b/.common/utils/helpers.js @@ -118,25 +118,59 @@ async function GetAvatar(username, platform) { console.debug(`No avatar found for ${username} (${platform}). Retrieving from Kick.`); let url = `https://kick.com/api/v2/channels/${username}`; + try { let response = await fetch(url); if (!response.ok) { - // Retry with underscores replaced by dashes const altUsername = username.replace(/_/g, "-"); url = `https://kick.com/api/v2/channels/${altUsername}`; response = await fetch(url); + if (!response.ok) { throw new Error(`HTTP error ${response.status}`); } } let data = await response.json(); - let avatarURL = data.user?.profile_pic || 'https://kick.com/img/default-profile-pictures/default2.jpeg'; + + const defaultAvatars = [ + 'https://kick.com/img/default-profile-pictures/default-avatar-1.webp', + 'https://kick.com/img/default-profile-pictures/default-avatar-2.webp', + 'https://kick.com/img/default-profile-pictures/default-avatar-3.webp', + 'https://kick.com/img/default-profile-pictures/default-avatar-4.webp', + 'https://kick.com/img/default-profile-pictures/default-avatar-5.webp', + 'https://kick.com/img/default-profile-pictures/default-avatar-6.webp', + ]; + + const avatarURL = + data.user?.profile_pic || + defaultAvatars[hashString(`${username}-${platform}`) % defaultAvatars.length]; + avatarMap.set(`${username}-${platform}`, avatarURL); return avatarURL; + } catch (error) { console.error("Failed to fetch avatar:", error.message); - return 'https://kick.com/img/default-profile-pictures/default2.jpeg'; + + const defaultAvatars = [ + 'https://kick.com/img/default-profile-pictures/default-avatar-1.webp', + 'https://kick.com/img/default-profile-pictures/default-avatar-2.webp', + 'https://kick.com/img/default-profile-pictures/default-avatar-3.webp', + 'https://kick.com/img/default-profile-pictures/default-avatar-4.webp', + 'https://kick.com/img/default-profile-pictures/default-avatar-5.webp', + 'https://kick.com/img/default-profile-pictures/default-avatar-6.webp', + ]; + + return defaultAvatars[hashString(`${username}-${platform}`) % defaultAvatars.length]; + } + + function hashString(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = ((hash << 5) - hash) + str.charCodeAt(i); + hash |= 0; // convert to 32-bit int + } + return Math.abs(hash); } } } From 8f16909da500e2319c224d25f26ccf8c31ef8133 Mon Sep 17 00:00:00 2001 From: nutty Date: Sun, 19 Apr 2026 06:51:32 +1000 Subject: [PATCH 02/11] Updated to support Streamer.bot v1.0.5-alpha3 --- .common/utils/helpers.js | 11 +++++ horizontal-chat/script.js | 53 +++++++++++++--------- multichat-overlay/script.js | 87 +++++++++++++++++++++++------------- multistream-alerts/script.js | 23 +++++++--- 4 files changed, 113 insertions(+), 61 deletions(-) diff --git a/.common/utils/helpers.js b/.common/utils/helpers.js index 05009eb..02fb2e4 100644 --- a/.common/utils/helpers.js +++ b/.common/utils/helpers.js @@ -335,6 +335,15 @@ function ConstructMessageFromParts(parts) { case "text": return EscapeHTML(part.text); case "cheer": + // TODO: It seems like Streamer.bot v1.0.5-alpha3 doesn't include the imageUrl for bits in the message parts, only the bits count. We may need to hardcode the image URL format for Twitch bits, or find another way to retrieve it. + let imageUrl = ''; + + if (!part.imageUrl) { + const tiers = [100000, 10000, 5000, 1000, 100, 10, 1]; + const activeTier = tiers.find(tier => part.bits >= tier) || 1; + part.imageUrl = `https://d3aqoihi2n8ty8.cloudfront.net/actions/cheer/dark/animated/${activeTier}/4.gif`; + } + // Render the cheer emote image const emoteImg = `${EscapeHTML(part.text)}`; @@ -342,6 +351,8 @@ function ConstructMessageFromParts(parts) { const bitLabel = `${EscapeHTML(part.bits.toString())}`; return emoteImg + bitLabel; + case "mention": + return ''; default: return `${EscapeHTML(part.text)}`; } diff --git a/horizontal-chat/script.js b/horizontal-chat/script.js index 1d429e8..46fb051 100644 --- a/horizontal-chat/script.js +++ b/horizontal-chat/script.js @@ -85,7 +85,9 @@ const animationSpeed = GetIntParam("animationSpeed", 0.5); const randomYouTubeColors = GetBooleanParam("randomYouTubeColors", false); const youtubeColor = urlParams.get("youtubeColor") || "#f70000"; const youtubeCustomSubIcon = urlParams.get("youtubeCustomSubIcon") || ""; -const kickUsername = urlParams.get("kickUsername") || ""; +let twitchUsername = urlParams.get("twitchUsername") || ""; +let kickUsername = urlParams.get("kickUsername") || ""; +let youtubeUsername = urlParams.get("youtubeUsername") || ""; @@ -343,12 +345,18 @@ client.on('Fourthwall.GiftDrawEnded', (response) => { // Connect and handle Pusher WebSocket async function KickConnect() { + // Fetch from Streamer.bot + const broadcasterInfo = await client.getBroadcaster(); + + // This code has nothing to do with Kick, but it's a really good place to get Twitch and YouTube usernames as well, because I'm built different like that + if (broadcasterInfo.platforms.twitch) + twitchUsername = broadcasterInfo.platforms.twitch.broadcastUserName; + if (broadcasterInfo.platforms.youtube) + youtubeUsername = broadcasterInfo.platforms.youtube.broadcastUserName; + // If user has not manually set Kick username, try to grab if from Streamer.bot if (!kickUsername) - { - // Fetch from Streamer.bot - const broadcasterInfo = await client.getBroadcaster(); - + { if (broadcasterInfo.platforms.kick) kickUsername = broadcasterInfo.platforms.kick.broadcasterLogin; else @@ -504,11 +512,11 @@ async function TwitchChatMessage(data) { return; // Don't post messages starting with "!" - if (data.message.message.startsWith("!") && excludeCommands) + if (data.text.startsWith("!") && excludeCommands) return; // Don't post messages from users from the ignore list - if (ignoreUserList.includes(data.message.username.toLowerCase())) + if (ignoreUserList.includes(data.user.login.toLowerCase())) return; // Get a reference to the template @@ -544,15 +552,15 @@ async function TwitchChatMessage(data) { // Set the username info if (showUsername) { - if (data.message.displayName.toLowerCase() == data.message.username.toLowerCase()) - usernameDiv.innerText = data.message.displayName; + if (data.user.name.toLowerCase() == data.user.login.toLowerCase()) + usernameDiv.innerText = data.user.name; else - usernameDiv.innerText = `${data.message.displayName} (${data.message.username})`; - usernameDiv.style.color = data.message.color; + usernameDiv.innerText = `${data.user.name} (${data.user.login})`; + usernameDiv.style.color = data.user.color; } // Set pronouns - const pronouns = await GetPronouns('twitch', data.message.username); + const pronouns = await GetPronouns('twitch', data.user.login); if (pronouns && showPronouns) { pronounsDiv.classList.add("pronouns"); pronounsDiv.innerText = pronouns; @@ -560,8 +568,8 @@ async function TwitchChatMessage(data) { // Set the message data let message = ConstructMessageFromParts(data.parts); - const messageColor = data.message.color; - const role = data.message.role; + const messageColor = data.user.color; + const role = data.user.role; // Set furry mode if (furryMode) @@ -573,7 +581,7 @@ async function TwitchChatMessage(data) { } // Set the "action" color - if (data.message.isMe) + if (data.meta.isMe) messageDiv.style.color = messageColor; // Render platform @@ -585,9 +593,9 @@ async function TwitchChatMessage(data) { // Render badges if (showBadges) { badgeListDiv.innerHTML = ""; - for (i in data.message.badges) { + for (i in data.user.badges) { const badge = new Image(); - badge.src = data.message.badges[i].imageUrl; + badge.src = data.user.badges[i].imageUrl; badge.classList.add("badge"); badgeListDiv.appendChild(badge); } @@ -595,7 +603,7 @@ async function TwitchChatMessage(data) { // Render avatars if (showAvatar) { - const username = data.message.username; + const username = data.user.login; const avatarURL = await GetAvatar(username, 'twitch'); const avatar = new Image(); avatar.src = avatarURL; @@ -604,7 +612,7 @@ async function TwitchChatMessage(data) { } // Custom styling for subs - if (data.message.subscriber) { + if (data.user.subscribed) { usernameDiv.classList.add('sub-glow') } @@ -623,7 +631,7 @@ async function TwitchChatMessage(data) { userInfoDiv.style.display = "none"; } - AddMessageItem(instance, data.message.msgId, 'twitch', data.user.id); + AddMessageItem(instance, data.messageId, 'twitch', data.user.id); } async function TwitchCheer(data) { @@ -798,8 +806,9 @@ async function TwitchWatchStreak(data) { if (!showTwitchWatchStreaks) return; - const displayName = data.displayName; - const watchStreak = data.watchStreak; + // TODO: Streamer.bot v1.0.5-alpha3 changed the data sent with this event, so for backwards compatibility we need to check for both the old and new properties + const displayName = data.displayName ?? data.user.name; + const watchStreak = data.watchStreak ?? data.streak_count; let message = `${displayName} is currently on a ${watchStreak} stream streak!`; diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 23f6c1d..bd80278 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -91,7 +91,9 @@ const animationSpeed = GetIntParam("animationSpeed", 0.1); const randomYouTubeColors = GetBooleanParam("randomYouTubeColors", false); const youtubeColor = urlParams.get("youtubeColor") || "#f70000"; const youtubeCustomSubIcon = urlParams.get("youtubeCustomSubIcon") || ""; +let twitchUsername = urlParams.get("twitchUsername") || ""; let kickUsername = urlParams.get("kickUsername") || ""; +let youtubeUsername = urlParams.get("youtubeUsername") || ""; @@ -383,12 +385,20 @@ client.on('Fourthwall.GiftDrawEnded', (response) => { // Connect and handle Pusher WebSocket async function KickConnect() { + // Fetch from Streamer.bot + const broadcasterInfo = await client.getBroadcaster(); + + console.log('Broadcaster info:', broadcasterInfo); + + // This code has nothing to do with Kick, but it's a really good place to get Twitch and YouTube usernames as well, because I'm built different like that + if (broadcasterInfo.platforms.twitch) + twitchUsername = broadcasterInfo.platforms.twitch.broadcastUserName; + if (broadcasterInfo.platforms.youtube) + youtubeUsername = broadcasterInfo.platforms.youtube.broadcastUserName; + // If user has not manually set Kick username, try to grab if from Streamer.bot if (!kickUsername) { - // Fetch from Streamer.bot - const broadcasterInfo = await client.getBroadcaster(); - if (broadcasterInfo.platforms.kick) kickUsername = broadcasterInfo.platforms.kick.broadcasterLogin; else @@ -551,11 +561,11 @@ async function TwitchChatMessage(data) { return; // Don't post messages starting with "!" - if (data.message.message.startsWith("!") && excludeCommands) + if (data.text.startsWith("!") && excludeCommands) return; // Don't post messages from users from the ignore list - if (ignoreUserList.includes(data.message.username.toLowerCase())) + if (ignoreUserList.includes(data.user.login.toLowerCase())) return; // Get a reference to the template @@ -593,7 +603,7 @@ async function TwitchChatMessage(data) { } // Set First Time Chatter - const firstMessage = data.message.firstMessage; + const firstMessage = data.meta.firstMessage; if (firstMessage && showMessage) { firstMessageDiv.style.display = 'block'; messageContainerDiv.classList.add("highlightMessage"); @@ -622,10 +632,10 @@ async function TwitchChatMessage(data) { } // Set Reply Message - const isReply = data.message.isReply; + const isReply = data.isReply; if (isReply && showMessage) { - const replyUser = data.message.reply.userName; - const replyMsg = data.message.reply.msgBody; + const replyUser = data.reply.userName; + const replyMsg = data.reply.msgBody; replyDiv.style.display = 'block'; replyUserDiv.innerText = replyUser; @@ -640,15 +650,15 @@ async function TwitchChatMessage(data) { // Set the username info if (showUsername) { - if (data.message.displayName.toLowerCase() == data.message.username.toLowerCase()) - usernameDiv.innerText = data.message.displayName; + if (data.user.name.toLowerCase() == data.user.login.toLowerCase()) + usernameDiv.innerText = data.user.name; else - usernameDiv.innerText = `${data.message.displayName} (${data.message.username})`; - usernameDiv.style.color = data.message.color; + usernameDiv.innerText = `${data.user.name} (${data.user.login})`; + usernameDiv.style.color = data.user.color; } // Set pronouns - const pronouns = await GetPronouns('twitch', data.message.username); + const pronouns = await GetPronouns('twitch', data.user.login); if (pronouns && showPronouns) { pronounsDiv.classList.add("pronouns"); pronounsDiv.innerText = pronouns; @@ -656,8 +666,14 @@ async function TwitchChatMessage(data) { // Set the message data let message = ConstructMessageFromParts(data.parts); - const messageColor = data.message.color; - const role = data.message.role; + const messageColor = data.user.color; + const role = data.user.role; + + // Highlight mentions + const mentionRgx = new RegExp(`(^|\\s)@${twitchUsername}(\\s|$)`, 'i'); + const mention = mentionRgx.test(data.text); + if (mention && showMessage) + messageContainerDiv.classList.add("highlightMessage"); // Set furry mode if (furryMode) @@ -669,7 +685,7 @@ async function TwitchChatMessage(data) { } // Set the "action" color - if (data.message.isMe) + if (data.meta.isMe) messageDiv.style.color = messageColor; // Remove the line break @@ -688,9 +704,9 @@ async function TwitchChatMessage(data) { // Render badges if (showBadges) { badgeListDiv.innerHTML = ""; - for (i in data.message.badges) { + for (i in data.user.badges) { const badge = new Image(); - badge.src = data.message.badges[i].imageUrl; + badge.src = data.user.badges[i].imageUrl; badge.classList.add("badge"); badgeListDiv.appendChild(badge); } @@ -698,7 +714,7 @@ async function TwitchChatMessage(data) { // Render avatars if (showAvatar) { - const username = data.message.username; + const username = data.user.login; const avatarURL = await GetAvatar(username, 'twitch'); const avatar = new Image(); avatar.src = avatarURL; @@ -707,7 +723,7 @@ async function TwitchChatMessage(data) { } // Custom styling for subs - if (data.message.subscriber) { + if (data.user.subscribed) { usernameDiv.classList.add('sub-glow') } @@ -740,7 +756,7 @@ async function TwitchChatMessage(data) { messageDiv.innerHTML = ''; messageDiv.appendChild(image); - AddMessageItem(instance, data.message.msgId, 'twitch', data.user.id); + AddMessageItem(instance, data.messageId, 'twitch', data.user.id); }; const urlObj = new URL(message); @@ -750,7 +766,7 @@ async function TwitchChatMessage(data) { image.src = "https://external-content.duckduckgo.com/iu/?u=" + urlObj.toString(); } else { - AddMessageItem(instance, data.message.msgId, 'twitch', data.user.id); + AddMessageItem(instance, data.messageId, 'twitch', data.user.id); } // Render YouTube links @@ -1227,9 +1243,10 @@ async function TwitchWatchStreak(data) { avatar.classList.add("avatar"); avatarDiv.appendChild(avatar); } - - const displayName = data.displayName; - const watchStreak = data.watchStreak; + + // TODO: Streamer.bot v1.0.5-alpha3 changed the data sent with this event, so for backwards compatibility we need to check for both the old and new properties + const displayName = data.displayName ?? data.user.name; + const watchStreak = data.watchStreak ?? data.streak_count; const message = RenderMessageWithEmotesHTML(data.message, data.emotes); titleDiv.innerText = `${displayName} is currently on a ${watchStreak} stream streak! `; @@ -1352,6 +1369,12 @@ async function YouTubeMessage(data) { //let message = RenderMessageWithEmotesHTML(data.message, data.emotes); let message = ConstructMessageFromParts(data.parts); + // Highlight mentions + const mentionRgx = new RegExp(`(^|\\s)@${youtubeUsername}(\\s|$)`, 'i'); + const mention = mentionRgx.test(message); + if (mention && showMessage) + messageContainerDiv.classList.add("highlightMessage"); + // Set furry mode if (furryMode) message = TranslateToFurry(message); @@ -2300,8 +2323,8 @@ async function KickChatMessage(data) { // Set Reply Message const isReply = data.isReply; if (isReply && showMessage) { - const replyUser = data.reply.sender.name; - const replyMsg = data.reply.content; + const replyUser = data.reply.userName; + const replyMsg = data.reply.msgBody; replyDiv.style.display = 'block'; replyUserDiv.innerText = replyUser; @@ -3171,13 +3194,13 @@ function IsThisUserAllowedToPostImagesOrNotReturnTrueIfTheyCanReturnFalseIfTheyC function GetPermissionLevel(data, platform) { switch (platform) { case 'twitch': - if (data.message.role >= 4) + if (data.user.role >= 4) return 40; - else if (data.message.role >= 3) + else if (data.user.role >= 3) return 30; - else if (data.message.role >= 2) + else if (data.user.role >= 2) return 20; - else if (data.message.role >= 2 || data.message.subscriber) + else if (data.user.role >= 2 || data.user.subscribed) return 15; else return 10; diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js index ed91179..8d4c8dc 100644 --- a/multistream-alerts/script.js +++ b/multistream-alerts/script.js @@ -115,7 +115,9 @@ const fourthwallAlertAction = urlParams.get("fourthwallAlertAction") || ""; // HIDDEN OPTIONS // //////////////////// +let twitchUsername = urlParams.get("twitchUsername") || ""; let kickUsername = urlParams.get("kickUsername") || ""; +let youtubeUsername = urlParams.get("youtubeUsername") || ""; // Set avatar visibility if (!showAvatar) { @@ -324,12 +326,18 @@ client.on('Fourthwall.GiftDrawEnded', (response) => { // Connect and handle Pusher WebSocket async function KickConnect() { + // Fetch from Streamer.bot + const broadcasterInfo = await client.getBroadcaster(); + + // This code has nothing to do with Kick, but it's a really good place to get Twitch and YouTube usernames as well, because I'm built different like that + if (broadcasterInfo.platforms.twitch) + twitchUsername = broadcasterInfo.platforms.twitch.broadcastUserName; + if (broadcasterInfo.platforms.youtube) + youtubeUsername = broadcasterInfo.platforms.youtube.broadcastUserName; + // If user has not manually set Kick username, try to grab if from Streamer.bot if (!kickUsername) - { - // Fetch from Streamer.bot - const broadcasterInfo = await client.getBroadcaster(); - + { if (broadcasterInfo.platforms.kick) kickUsername = broadcasterInfo.platforms.kick.broadcasterLogin; else @@ -494,7 +502,7 @@ async function TwitchCheer(data) { return; // Set the text - const username = data.message.displayName; + const username = data.user.login; const bits = data.bits; let message = ConstructMessageFromParts(data.parts); @@ -720,8 +728,9 @@ async function TwitchWatchStreak(data) { const avatarURL = await GetAvatar(data.userName, 'twitch'); // Set the text - const username = data.displayName; - const watchStreak = data.watchStreak; + // TODO: Streamer.bot v1.0.5-alpha3 changed the data sent with this event, so for backwards compatibility we need to check for both the old and new properties + const displayName = data.displayName ?? data.user.name; + const watchStreak = data.watchStreak ?? data.streak_count; UpdateAlertBox( 'twitch', From a89b03074c2a23e6e6140c652bb8ddf2b784b671 Mon Sep 17 00:00:00 2001 From: nutty Date: Sun, 19 Apr 2026 07:05:24 +1000 Subject: [PATCH 03/11] Fixed Twitch mentions --- .common/utils/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.common/utils/helpers.js b/.common/utils/helpers.js index 02fb2e4..a54e3ae 100644 --- a/.common/utils/helpers.js +++ b/.common/utils/helpers.js @@ -352,7 +352,7 @@ function ConstructMessageFromParts(parts) { return emoteImg + bitLabel; case "mention": - return ''; + return part.text; default: return `${EscapeHTML(part.text)}`; } From 9bf1efb46abc96df06ae8391c89ccba280044fe9 Mon Sep 17 00:00:00 2001 From: nutty Date: Tue, 28 Apr 2026 17:13:12 +1000 Subject: [PATCH 04/11] Brought over .common from nutty.gg premium --- .common/core/settings-core/script.js | 18 +++ .common/core/settings-core/style.css | 11 ++ .common/styles/global.css | 109 +++++++++++++++++- .common/templates/poll-response-template.css | 95 +++++++++++++++ .common/templates/poll-response-template.html | 16 +++ 5 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 .common/templates/poll-response-template.css create mode 100644 .common/templates/poll-response-template.html diff --git a/.common/core/settings-core/script.js b/.common/core/settings-core/script.js index 339f7a8..3e76669 100644 --- a/.common/core/settings-core/script.js +++ b/.common/core/settings-core/script.js @@ -70,6 +70,12 @@ function LoadJSON(settingsJson) { settingItem.classList.add('setting-item'); settingItem.id = `item-${setting.id}`; + // Indent settings that are dependent on other settings using the showIf property + if (setting.showIf) { + const depth = GetSettingDepth(setting, data.settings); + settingItem.style.marginLeft = `${depth * 20}px`; + } + const labelDescriptionDiv = document.createElement('div'); if (setting.label) { @@ -469,6 +475,18 @@ window.addEventListener('message', (event) => { } }); +function GetSettingDepth(setting, allSettings) { + let depth = 0; + let current = setting; + + while (current.showIf) { + depth++; + current = allSettings.find(s => s.id === current.showIf) || {}; + } + + return depth; +} + diff --git a/.common/core/settings-core/style.css b/.common/core/settings-core/style.css index bbfbf7e..87b86ba 100644 --- a/.common/core/settings-core/style.css +++ b/.common/core/settings-core/style.css @@ -53,6 +53,7 @@ body { height: 100%; padding: 0px 20px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 1); + z-index: 2; } #settingsPanel::-webkit-scrollbar { @@ -82,6 +83,16 @@ body { flex-grow: 1; overflow: hidden; position: relative; + + /* Dark Checkerboard Pattern */ + background-color: #181818; /* The "base" dark color */ + background-image: conic-gradient( + #252525 90deg, + #181818 90deg 180deg, + #252525 180deg 270deg, + #181818 270deg + ); + background-size: 30px 30px; /* Size of the squares */ } #widgetPreview { diff --git a/.common/styles/global.css b/.common/styles/global.css index 1f4bca5..9cd07b1 100644 --- a/.common/styles/global.css +++ b/.common/styles/global.css @@ -28,7 +28,7 @@ body { padding: 0; font-family: Inter, system-ui, sans-serif; /* font-family: "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", sans-serif; */ - /* font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif; */ + /* font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif; */ color: white; background-color: var(--background-color); } @@ -311,6 +311,17 @@ iframe { /*** DIALOG ***/ /**************/ +.dialog-wrapper { + position: fixed; + width: 100%; + height: 100%; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + z-index: 1000; + backdrop-filter: blur(var(--blur-intensity)); +} + .dialog { font-size: 1em; background-color: var(--dialog-background); @@ -327,7 +338,7 @@ iframe { border: 1px solid #40404080; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); z-index: 1000; - /* above overlay */ + backdrop-filter: blur(var(--blur-intensity)); } .dialog-nav-button { @@ -384,4 +395,98 @@ select { color: #fff; cursor: pointer; min-width: 8em; +} + +/***************/ +/*** SIDEBAR ***/ +/***************/ + +#sidebar { + position: fixed; + top: 0; + left: 0; + width: 250px; + /* expanded width */ + height: 100vh; + color: #fff; + overflow: hidden; + transition: width 0.3s ease; + backdrop-filter: blur(var(--blur-intensity)); + /* <-- adds the blur */ + box-shadow: 0 0 10px rgba(0, 0, 0, 1); +} + +#sidebar.collapsed { + width: 3em; + /* collapsed width */ +} + +#sidebar button { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 0.5em; + padding: 0.5em 1em; + border-radius: 0; +} + +#sidebar .sidebar-menu { + opacity: 1; + transition: opacity 0.3s ease; +} + +#sidebar.collapsed .sidebar-menu { + opacity: 1; +} + +#collapse-sidebar-btn { + width: 100%; + background: none; + border: none; + color: #fff; + cursor: pointer; + justify-content: flex-end !important; +} + +.sidebar-item { + padding: 0.75em 1em; + text-align: left; + background: none; + border: none; + color: inherit; + font-size: 1em; + cursor: pointer; + white-space: nowrap; + /* prevent line breaks */ + overflow: hidden; + /* hide overflow text */ + text-overflow: ellipsis; + /* optional: show "..." if needed */ +} + +#sidebar.collapsed .sidebar-item { + justify-content: center; + padding: 0.75em 0; +} + +.sidebar-label { + display: inline-block; +} + +#sidebar.collapsed .sidebar-label { + display: none; +} + +.sidebar-item svg { + width: 1em; + height: 1em; + flex-shrink: 0; +} + +.sidebar-spacer { + flex: 1; +} + +.sidebar-item:hover { + background-color: #333; } \ No newline at end of file diff --git a/.common/templates/poll-response-template.css b/.common/templates/poll-response-template.css new file mode 100644 index 0000000..29ef7b5 --- /dev/null +++ b/.common/templates/poll-response-template.css @@ -0,0 +1,95 @@ +:root { + --winning-color: #3be47680; + --vote-bar-color: #FFFFFF; + --vote-bar-track-color: #FFFFFF33; +} + +.response-container { + display: flex; + align-items: center; + gap: 0.75em; + width: 100%; + transition: all 0.8s ease; +} + +.response-index { + width: 2.5em; + height: 2.5em; + min-width: 2.5em; + border-radius: 50%; + background-color: rgba(255, 255, 255, 0.15); + display: flex; + align-items: center; + justify-content: center; + font-weight: 900; + font-size: 0.8em; + transition: all 0.8s ease-out; +} + +.response-except-just-the-shit-on-the-right-side { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.25em; + min-width: 0; +} + +.response-title, +.response-votes { + position: relative; + z-index: 1; + display: flex; + align-items: center; + line-height: 1.2em; + /* mix-blend-mode: exclusion; */ /* I couldn't make this shit look good so I'm turning it off for now */ +} + +.response-title { + flex: 1 1 auto; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.response-votes { + flex: 0 0 auto; + white-space: nowrap; + margin-left: 8px; + font-size: 0.8em; +} + +.response-bar-track { + background-color: var(--vote-bar-track-color); + position: relative; + display: flex; + border-radius: 0.5em; + overflow: hidden; + padding: 0.5em 0.5em; +} + +.response-bar-fill { + position: absolute; /* take it out of flex flow */ + top: 0; + left: 0; + height: 100%; + width: 100%; + background-color: var(--vote-bar-color); + transition: all 0.25s ease; + z-index: 0; /* sits behind text */ + inset: 0; + display: flex; /* 👈 KEY CHANGE */ + overflow: hidden; +} + +.trophy-icon { + width: 1.8em; + height: 1.8em; + vertical-align: middle; +} + +.response-bar-segment { + height: 100%; + flex-shrink: 0; + transition: all 0.25s ease; +} \ No newline at end of file diff --git a/.common/templates/poll-response-template.html b/.common/templates/poll-response-template.html new file mode 100644 index 0000000..5998cb6 --- /dev/null +++ b/.common/templates/poll-response-template.html @@ -0,0 +1,16 @@ + \ No newline at end of file From 07c1b82a972b911f29d80a91b8821044a1fa4e0a Mon Sep 17 00:00:00 2001 From: nutty Date: Sun, 10 May 2026 20:58:57 +1000 Subject: [PATCH 05/11] Fixed Twitch Watch Streaks showing incorrectly --- multichat-overlay/script.js | 4 ++-- multistream-alerts/script.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index bd80278..3f12e31 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -1236,7 +1236,7 @@ async function TwitchWatchStreak(data) { if (showAvatar) { // Render avatars - const username = data.userName; + const username = data.user.login; const avatarURL = await GetAvatar(username, 'twitch'); const avatar = new Image(); avatar.src = avatarURL; @@ -1250,7 +1250,7 @@ async function TwitchWatchStreak(data) { const message = RenderMessageWithEmotesHTML(data.message, data.emotes); titleDiv.innerText = `${displayName} is currently on a ${watchStreak} stream streak! `; - contentDiv.innerHTML = message; + contentDiv.innerHTML = ConstructMessageFromParts(data.parts); AddMessageItem(instance, data.messageId); } diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js index 8d4c8dc..e7c8587 100644 --- a/multistream-alerts/script.js +++ b/multistream-alerts/script.js @@ -725,7 +725,7 @@ async function TwitchWatchStreak(data) { return; // Render avatars - const avatarURL = await GetAvatar(data.userName, 'twitch'); + const avatarURL = await GetAvatar(data.user.login, 'twitch'); // Set the text // TODO: Streamer.bot v1.0.5-alpha3 changed the data sent with this event, so for backwards compatibility we need to check for both the old and new properties @@ -735,10 +735,10 @@ async function TwitchWatchStreak(data) { UpdateAlertBox( 'twitch', avatarURL, - `${username}`, + `${displayName}`, `is currently on a ${watchStreak} stream streak!`, '', - username, + displayName, '', twitchWatchStreaksAction, data From e32387d9063c7abef2c2da00a100d4a06a4a3a32 Mon Sep 17 00:00:00 2001 From: nutty Date: Sun, 10 May 2026 21:01:09 +1000 Subject: [PATCH 06/11] Fixed Twitch Watch Streaks showing incorrectly --- multichat-overlay/script.js | 1 - 1 file changed, 1 deletion(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 3f12e31..c1cdd15 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -1247,7 +1247,6 @@ async function TwitchWatchStreak(data) { // TODO: Streamer.bot v1.0.5-alpha3 changed the data sent with this event, so for backwards compatibility we need to check for both the old and new properties const displayName = data.displayName ?? data.user.name; const watchStreak = data.watchStreak ?? data.streak_count; - const message = RenderMessageWithEmotesHTML(data.message, data.emotes); titleDiv.innerText = `${displayName} is currently on a ${watchStreak} stream streak! `; contentDiv.innerHTML = ConstructMessageFromParts(data.parts); From e4761eae791de57e6d4e19fe51fa40ed9e3d1be1 Mon Sep 17 00:00:00 2001 From: nutty Date: Sun, 10 May 2026 21:11:07 +1000 Subject: [PATCH 07/11] Added toggle for "Highlight Mentions" --- multichat-overlay/script.js | 7 ++++--- multichat-overlay/settings/settings.json | 8 ++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index c1cdd15..a7cdbb4 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -42,6 +42,7 @@ const ignoreChatters = urlParams.get("ignoreChatters") || ""; const scrollDirection = GetIntParam("scrollDirection", 1); const groupConsecutiveMessages = GetBooleanParam("groupConsecutiveMessages", false); const inlineChat = GetBooleanParam("inlineChat", false); +const highlightMentions = GetBooleanParam("highlightMentions", true); const imageEmbedPermissionLevel = GetIntParam("imageEmbedPermissionLevel", 20); const showYouTubeLinkPreviews = GetBooleanParam("showYouTubeLinkPreviews", true); @@ -672,7 +673,7 @@ async function TwitchChatMessage(data) { // Highlight mentions const mentionRgx = new RegExp(`(^|\\s)@${twitchUsername}(\\s|$)`, 'i'); const mention = mentionRgx.test(data.text); - if (mention && showMessage) + if (mention && highlightMentions && showMessage) messageContainerDiv.classList.add("highlightMessage"); // Set furry mode @@ -1371,7 +1372,7 @@ async function YouTubeMessage(data) { // Highlight mentions const mentionRgx = new RegExp(`(^|\\s)@${youtubeUsername}(\\s|$)`, 'i'); const mention = mentionRgx.test(message); - if (mention && showMessage) + if (mention && highlightMentions && showMessage) messageContainerDiv.classList.add("highlightMessage"); // Set furry mode @@ -2348,7 +2349,7 @@ async function KickChatMessage(data) { // Highlight mentions const mentionRgx = new RegExp(`(^|\\s)@${kickUsername}(\\s|$)`, 'i'); const mention = mentionRgx.test(message); - if (mention && showMessage) + if (mention && highlightMentions && showMessage) messageContainerDiv.classList.add("highlightMessage"); // Set furry mode diff --git a/multichat-overlay/settings/settings.json b/multichat-overlay/settings/settings.json index 68d2f89..12e8da0 100644 --- a/multichat-overlay/settings/settings.json +++ b/multichat-overlay/settings/settings.json @@ -192,6 +192,14 @@ "defaultValue": false, "group": "General" }, + { + "id": "highlightMentions", + "label": "Highlight Mentions", + "description": "Highlight messages that mention the streamer", + "type": "checkbox", + "defaultValue": true, + "group": "General" + }, { "id": "imageEmbedPermissionLevel", "label": "Embed Images", From f92ffcc5daa4ba736a8f1c01f7c40b3aab9eaff2 Mon Sep 17 00:00:00 2001 From: nutty Date: Sun, 10 May 2026 21:44:34 +1000 Subject: [PATCH 08/11] Added support for Twitch Power Ups --- horizontal-chat/icons/badges/twitch-bit.svg | 7 +++ horizontal-chat/script.js | 23 ++++++++ horizontal-chat/settings/settings.json | 8 +++ multichat-overlay/icons/badges/twitch-bit.svg | 7 +++ multichat-overlay/script.js | 52 +++++++++++++++++++ multichat-overlay/settings/settings.json | 8 +++ .../icons/badges/twitch-bit.svg | 7 +++ multistream-alerts/script.js | 35 +++++++++++++ multistream-alerts/settings/settings.json | 17 ++++++ 9 files changed, 164 insertions(+) create mode 100644 horizontal-chat/icons/badges/twitch-bit.svg create mode 100644 multichat-overlay/icons/badges/twitch-bit.svg create mode 100644 multistream-alerts/icons/badges/twitch-bit.svg diff --git a/horizontal-chat/icons/badges/twitch-bit.svg b/horizontal-chat/icons/badges/twitch-bit.svg new file mode 100644 index 0000000..5fcda08 --- /dev/null +++ b/horizontal-chat/icons/badges/twitch-bit.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/horizontal-chat/script.js b/horizontal-chat/script.js index 46fb051..1c28a25 100644 --- a/horizontal-chat/script.js +++ b/horizontal-chat/script.js @@ -46,6 +46,7 @@ const showTwitchAnnouncements = GetBooleanParam("showTwitchAnnouncements", true) const showTwitchFollows = GetBooleanParam("showTwitchFollows", false); const showTwitchSubs = GetBooleanParam("showTwitchSubs", true); const showTwitchChannelPointRedemptions = GetBooleanParam("showTwitchChannelPointRedemptions", true); +const showTwitchPowerUpRedemptions = GetBooleanParam("showTwitchPowerUpRedemptions", true); const showTwitchRaids = GetBooleanParam("showTwitchRaids", true); const showTwitchWatchStreaks = GetBooleanParam("showTwitchWatchStreaks", true); const showTwitchSharedChat = GetBooleanParam("showTwitchSharedChat", true); @@ -181,6 +182,11 @@ client.on('Twitch.RewardRedemption', (response) => { TwitchRewardRedemption(response.data); }) +client.on('Twitch.CustomPowerUpRedemption', (response) => { + console.debug(response.data); + TwitchCustomPowerUpRedemption(response.data); +}) + client.on('Twitch.Raid', (response) => { console.debug(response.data); TwitchRaid(response.data); @@ -788,6 +794,23 @@ async function TwitchRewardRedemption(data) { ShowAlert(message, 'twitch'); } +async function TwitchCustomPowerUpRedemption(data) { + if (!showTwitchPowerUpRedemptions) + return; + + let username = data.user.name; + if (data.user.name.toLowerCase() != data.user.login.toLowerCase()) + username = `${data.user.name} (${data.user.login})`; + const powerUpName = data.custom_power_up.title; + const cost = data.custom_power_up.bits_cost; + const prompt = data.custom_power_up.prompt; + const bitIcon = ``; + + let message = `${username} redeemed ${powerUpName} ${bitIcon} ${cost}`; + + ShowAlert(message, 'twitch'); +} + async function TwitchRaid(data) { if (!showTwitchRaids) return; diff --git a/horizontal-chat/settings/settings.json b/horizontal-chat/settings/settings.json index bf48dea..a984361 100644 --- a/horizontal-chat/settings/settings.json +++ b/horizontal-chat/settings/settings.json @@ -175,6 +175,14 @@ "defaultValue": true, "group": "Which Twitch messages do you want to see?" }, + { + "id": "showTwitchPowerUpRedemptions", + "label": "Power-Up Redemptions", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Twitch messages do you want to see?" + }, { "id": "showTwitchRaids", "label": "Raids", diff --git a/multichat-overlay/icons/badges/twitch-bit.svg b/multichat-overlay/icons/badges/twitch-bit.svg new file mode 100644 index 0000000..5fcda08 --- /dev/null +++ b/multichat-overlay/icons/badges/twitch-bit.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index a7cdbb4..78c6ec0 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -52,6 +52,7 @@ const showTwitchAnnouncements = GetBooleanParam("showTwitchAnnouncements", true) const showTwitchFollows = GetBooleanParam("showTwitchFollows", false); const showTwitchSubs = GetBooleanParam("showTwitchSubs", true); const showTwitchChannelPointRedemptions = GetBooleanParam("showTwitchChannelPointRedemptions", true); +const showTwitchPowerUpRedemptions = GetBooleanParam("showTwitchPowerUpRedemptions", true); const showTwitchRaids = GetBooleanParam("showTwitchRaids", true); const showTwitchWatchStreaks = GetBooleanParam("showTwitchWatchStreaks", true); const showTwitchSharedChat = GetIntParam("showTwitchSharedChat", 2); @@ -202,6 +203,11 @@ client.on('Twitch.RewardRedemption', (response) => { TwitchRewardRedemption(response.data); }) +client.on('Twitch.CustomPowerUpRedemption', (response) => { + console.debug(response.data); + TwitchCustomPowerUpRedemption(response.data); +}) + client.on('Twitch.Raid', (response) => { console.debug(response.data); TwitchRaid(response.data); @@ -1170,6 +1176,52 @@ async function TwitchRewardRedemption(data) { AddMessageItem(instance, data.messageId); } +async function TwitchCustomPowerUpRedemption(data) { + if (!showTwitchPowerUpRedemptions) + 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.user.login; + const avatarURL = await GetAvatar(username, 'twitch'); + const avatar = new Image(); + avatar.src = avatarURL; + avatar.classList.add("avatar"); + avatarDiv.appendChild(avatar); + } + + // Set the text + let username = data.user.name; + if (data.user.name.toLowerCase() != data.user.login.toLowerCase()) + username = `${data.user.name} (${data.user.login})`; + const powerUpName = data.custom_power_up.title; + const cost = data.custom_power_up.bits_cost; + const prompt = data.custom_power_up.prompt; + const bitIcon = ``; + + titleDiv.innerHTML = `${username} redeemed ${powerUpName} ${bitIcon} ${cost}`; + contentDiv.innerText = `${prompt}`; + + AddMessageItem(instance, data.messageId); +} + async function TwitchRaid(data) { if (!showTwitchRaids) return; diff --git a/multichat-overlay/settings/settings.json b/multichat-overlay/settings/settings.json index 12e8da0..6c53129 100644 --- a/multichat-overlay/settings/settings.json +++ b/multichat-overlay/settings/settings.json @@ -290,6 +290,14 @@ "defaultValue": true, "group": "Which Twitch messages do you want to see?" }, + { + "id": "showTwitchPowerUpRedemptions", + "label": "Power-Up Redemptions", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Twitch messages do you want to see?" + }, { "id": "showTwitchRaids", "label": "Raids", diff --git a/multistream-alerts/icons/badges/twitch-bit.svg b/multistream-alerts/icons/badges/twitch-bit.svg new file mode 100644 index 0000000..5fcda08 --- /dev/null +++ b/multistream-alerts/icons/badges/twitch-bit.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js index e7c8587..a344aff 100644 --- a/multistream-alerts/script.js +++ b/multistream-alerts/script.js @@ -63,6 +63,8 @@ const showTwitchSubs = GetBooleanParam("showTwitchSubs", true); const twitchSubAction = urlParams.get("twitchSubAction") || ""; const showTwitchChannelPointRedemptions = GetBooleanParam("showTwitchChannelPointRedemptions", true); const twitchChannelPointRedemptionAction = urlParams.get("twitchChannelPointRedemptionAction") || ""; +const showTwitchPowerUpRedemptions = GetBooleanParam("showTwitchPowerUpRedemptions", true); +const twitchPowerUpRedemptionAction = urlParams.get("twitchPowerUpRedemptionAction") || ""; const showTwitchCheers = GetBooleanParam("showTwitchCheers", true); const twitchCheerAction = urlParams.get("twitchCheerAction") || ""; const showTwitchRaids = GetBooleanParam("showTwitchRaids", true); @@ -218,6 +220,11 @@ client.on('Twitch.RewardRedemption', (response) => { TwitchRewardRedemption(response.data); }) +client.on('Twitch.CustomPowerUpRedemption', (response) => { + console.debug(response.data); + TwitchCustomPowerUpRedemption(response.data); +}) + client.on('Twitch.Raid', (response) => { console.debug(response.data); TwitchRaid(response.data); @@ -696,6 +703,34 @@ async function TwitchRewardRedemption(data) { ); } +async function TwitchCustomPowerUpRedemption(data) { + if (!showTwitchPowerUpRedemptions) + return; + + let username = data.user.name; + if (data.user.name.toLowerCase() != data.user.login.toLowerCase()) + username = `${data.user.name} (${data.user.login})`; + const powerUpName = data.custom_power_up.title; + const cost = data.custom_power_up.bits_cost; + const prompt = data.custom_power_up.prompt; + const bitIcon = ``; + + // Render avatars + const avatarURL = await GetAvatar(data.user.login, 'twitch'); + + UpdateAlertBox( + 'twitch', + avatarURL, + `${username} redeemed`, + `${powerUpName} ${bitIcon} ${cost}`, + '', + username, + prompt, + twitchPowerUpRedemptionAction, + data + ); +} + async function TwitchRaid(data) { if (!showTwitchRaids) return; diff --git a/multistream-alerts/settings/settings.json b/multistream-alerts/settings/settings.json index 61d4982..c919d79 100644 --- a/multistream-alerts/settings/settings.json +++ b/multistream-alerts/settings/settings.json @@ -278,6 +278,23 @@ "showIf": "showTwitchChannelPointRedemptions", "group": "Which Twitch alerts do you want to see?" }, + { + "id": "showTwitchPowerUpRedemptions", + "label": "Power-Up Redemptions", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Twitch alerts do you want to see?" + }, + { + "id": "twitchPowerUpRedemptionAction", + "label": "", + "description": "Also run this Streamer.bot Action", + "type": "sb-actions", + "defaultValue": "", + "showIf": "showTwitchPowerUpRedemptions", + "group": "Which Twitch alerts do you want to see?" + }, { "id": "showTwitchCheers", "label": "Cheers", From 4280ac69813d864e0fc64cdc455fe365a5f3fd1a Mon Sep 17 00:00:00 2001 From: nutty Date: Thu, 14 May 2026 05:29:02 +1000 Subject: [PATCH 09/11] Added basic support for Twitch Custom Power Ups --- printer-bot/contents/script.js | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/printer-bot/contents/script.js b/printer-bot/contents/script.js index 0dc74f5..5fa6563 100644 --- a/printer-bot/contents/script.js +++ b/printer-bot/contents/script.js @@ -189,6 +189,54 @@ async function CustomEvent(data) { SetPlatformIcon(iconEl, 'twitch'); } break; + case ('TwitchCustomPowerUpRedemption'): + { + avatarEl.src = await GetAvatar(data.userName, 'twitch'); + titleEl.innerText = `${data["customPowerUp.bitsCost"]} BITS`; + subtitleEl.innerText = `${data.user}`; + + const messageEl = document.createElement('div'); + messageEl.innerHTML = data.rawInput; + + // // Render emotes + // for (i in data.emotes) { + // const emoteElement = ``; + // const emoteName = EscapeRegExp(data.emotes[i].name); + // function EscapeRegExp(string) { + // return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + // } + + // let regexPattern = emoteName; + + // // Check if the emote name consists only of word characters (alphanumeric and underscore) + // if (/^\w+$/.test(emoteName)) { + // regexPattern = `\\b${emoteName}\\b`; + // } + // else { + // // For non-word emotes, ensure they are surrounded by non-word characters or boundaries + // regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`; + // } + + // const regex = new RegExp(regexPattern, 'g'); + // messageEl.innerHTML = messageEl.innerHTML.replace(regex, emoteElement); + // } + + // // Render cheermotes + // for (i in data.cheerEmotes) { + // const bits = data.cheerEmotes[i].bits; + // const imageUrl = data.cheerEmotes[i].imageUrl; + // const name = data.cheerEmotes[i].name; + // const cheerEmoteElement = ``; + // const bitsElements = `${bits}` + // messageEl.innerHTML = messageEl.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements); + // } + + contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'twitch'); + } + break; // YouTube Events case ('YouTubeNewSponsor'): From 66512a05dd6dd375d438affd0cef2e74f430d751 Mon Sep 17 00:00:00 2001 From: nutty Date: Thu, 14 May 2026 05:55:00 +1000 Subject: [PATCH 10/11] Twitch Custom Power-Ups now use the background color of the power-up, not a generic Purple --- multichat-overlay/script.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 78c6ec0..4ecde50 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -1195,7 +1195,12 @@ async function TwitchCustomPowerUpRedemption(data) { const contentDiv = instance.querySelector("#content"); // Set the card background colors - cardDiv.classList.add('twitch'); + // cardDiv.classList.add('twitch'); + const baseColor = data.custom_power_up.backgroundColor; // e.g., #3498db + + // B3 = 70% opacity + // FF = 100% opacity + cardDiv.style.background = `linear-gradient(180deg, ${baseColor}B3 0%, ${baseColor}FF 100%)`; if (showAvatar) { // Render avatars From 352521d64e3ddfda52f78e04163999d676888c09 Mon Sep 17 00:00:00 2001 From: nutty Date: Sun, 17 May 2026 08:25:28 +1000 Subject: [PATCH 11/11] Fixed Twitch Power Ups --- horizontal-chat/script.js | 6 +++--- multichat-overlay/script.js | 10 +++++----- multistream-alerts/script.js | 8 ++++---- printer-bot/contents/script.js | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/horizontal-chat/script.js b/horizontal-chat/script.js index 1c28a25..f5fc40f 100644 --- a/horizontal-chat/script.js +++ b/horizontal-chat/script.js @@ -801,9 +801,9 @@ async function TwitchCustomPowerUpRedemption(data) { let username = data.user.name; if (data.user.name.toLowerCase() != data.user.login.toLowerCase()) username = `${data.user.name} (${data.user.login})`; - const powerUpName = data.custom_power_up.title; - const cost = data.custom_power_up.bits_cost; - const prompt = data.custom_power_up.prompt; + const powerUpName = data.customPowerUp.title; + const cost = data.customPowerUp.bits; + const userInput = data.userInput; const bitIcon = ``; let message = `${username} redeemed ${powerUpName} ${bitIcon} ${cost}`; diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 4ecde50..49afc56 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -1196,7 +1196,7 @@ async function TwitchCustomPowerUpRedemption(data) { // Set the card background colors // cardDiv.classList.add('twitch'); - const baseColor = data.custom_power_up.backgroundColor; // e.g., #3498db + const baseColor = data.customPowerUp.backgroundColor; // e.g., #3498db // B3 = 70% opacity // FF = 100% opacity @@ -1216,13 +1216,13 @@ async function TwitchCustomPowerUpRedemption(data) { let username = data.user.name; if (data.user.name.toLowerCase() != data.user.login.toLowerCase()) username = `${data.user.name} (${data.user.login})`; - const powerUpName = data.custom_power_up.title; - const cost = data.custom_power_up.bits_cost; - const prompt = data.custom_power_up.prompt; + const powerUpName = data.customPowerUp.title; + const cost = data.customPowerUp.bits; + const userInput = data.userInput; const bitIcon = ``; titleDiv.innerHTML = `${username} redeemed ${powerUpName} ${bitIcon} ${cost}`; - contentDiv.innerText = `${prompt}`; + contentDiv.innerText = `${userInput}`; AddMessageItem(instance, data.messageId); } diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js index a344aff..f141c4b 100644 --- a/multistream-alerts/script.js +++ b/multistream-alerts/script.js @@ -710,9 +710,9 @@ async function TwitchCustomPowerUpRedemption(data) { let username = data.user.name; if (data.user.name.toLowerCase() != data.user.login.toLowerCase()) username = `${data.user.name} (${data.user.login})`; - const powerUpName = data.custom_power_up.title; - const cost = data.custom_power_up.bits_cost; - const prompt = data.custom_power_up.prompt; + const powerUpName = data.customPowerUp.title; + const cost = data.customPowerUp.bits; + const userInput = data.userInput; const bitIcon = ``; // Render avatars @@ -725,7 +725,7 @@ async function TwitchCustomPowerUpRedemption(data) { `${powerUpName} ${bitIcon} ${cost}`, '', username, - prompt, + userInput, twitchPowerUpRedemptionAction, data ); diff --git a/printer-bot/contents/script.js b/printer-bot/contents/script.js index 5fa6563..ccc444d 100644 --- a/printer-bot/contents/script.js +++ b/printer-bot/contents/script.js @@ -192,7 +192,7 @@ async function CustomEvent(data) { case ('TwitchCustomPowerUpRedemption'): { avatarEl.src = await GetAvatar(data.userName, 'twitch'); - titleEl.innerText = `${data["customPowerUp.bitsCost"]} BITS`; + titleEl.innerText = `${data["customPowerUp.bits"]} BITS`; subtitleEl.innerText = `${data.user}`; const messageEl = document.createElement('div');