Merge pull request #33 from nuttylmao/beta

Beta
This commit is contained in:
nutty
2026-09-01 16:19:00 +10:00
committed by GitHub
35 changed files with 655 additions and 158 deletions
+42
View File
@@ -188,6 +188,21 @@ function LoadJSON(settingsJson) {
await PopulateFontDatalist(); await PopulateFontDatalist();
}, { once: true }); }, { once: true });
break; break;
case 'timezone':
inputElement = document.createElement('input');
inputElement.type = 'text';
inputElement.placeholder = 'Type to search timezone...';
inputElement.id = setting.id;
inputElement.value = settingsMap.has(setting.id) ? settingsMap.get(setting.id) : setting.defaultValue;
inputElement.setAttribute('list', 'timezones');
inputElement.autocomplete = 'off';
// Populate datalist on focus (runs once)
inputElement.addEventListener('focus', function loadOnce() {
inputElement.removeEventListener('focus', loadOnce);
PopulateTimezoneDatalist();
}, { once: true });
break;
case 'button': case 'button':
inputElement = document.createElement('button'); inputElement = document.createElement('button');
inputElement.id = setting.id; //Added setting ID inputElement.id = setting.id; //Added setting ID
@@ -431,6 +446,30 @@ async function PopulateFontDatalist() {
} }
} }
function PopulateTimezoneDatalist() {
try {
// Fetch all official IANA timezones natively from the browser
const timezones = Intl.supportedValuesOf('timeZone');
// Create the datalist element
const datalistElement = document.createElement('datalist');
datalistElement.id = 'timezones';
// Append each timezone as an option
timezones.forEach(tz => {
const option = document.createElement('option');
option.value = tz;
datalistElement.appendChild(option);
});
document.body.appendChild(datalistElement);
console.debug(`Loaded ${timezones.length} timezones into auto-suggest.`);
} catch (err) {
console.error("Error generating timezone suggestions:", err);
}
}
///////////////////////// /////////////////////////
@@ -600,3 +639,6 @@ LoadJSON(settingsJson);
// Populate local fonts for auto-suggest // Populate local fonts for auto-suggest
PopulateFontDatalist(); PopulateFontDatalist();
// Populate timezones for auto-suggest
PopulateTimezoneDatalist();
+59 -15
View File
@@ -688,25 +688,69 @@ function VersionCheck(requiredVersion, installedVersion) {
const [rMajor, rMinor, rPatch] = requiredVersion.split('.').map(Number); const [rMajor, rMinor, rPatch] = requiredVersion.split('.').map(Number);
const [iMajor, iMinor, iPatch] = installedVersion.split('.').map(Number); const [iMajor, iMinor, iPatch] = installedVersion.split('.').map(Number);
// 1. CRITICAL: Major versions must match exactly. // Compare Major
if (iMajor !== rMajor) { if (iMajor > rMajor) return 'compatible';
console.log(`VersionCheck: Major version mismatch. Required: ${rMajor}, Installed: ${iMajor}`); if (iMajor < rMajor) return 'incompatible';
return 'incompatible';
// Major matches, compare Minor
if (iMinor > rMinor) return 'compatible';
if (iMinor < rMinor) return 'incompatible'; // or 'incompatible' depending on your policy
// Major and Minor match, compare Patch
if (iPatch >= rPatch) return 'compatible';
return 'soft-warning'; // Installed patch is lower than required
} }
// 2. SOFT WARNING: The installed minor version is lower than what is required. function SanitizeHTML(htmlString) {
if (iMinor < rMinor) { // 1. Parse the string into a temporary DOM document
console.log(`VersionCheck: Minor version mismatch. Required: ${rMinor}, Installed: ${iMinor}`); const parser = new DOMParser();
return 'soft-warning'; const doc = parser.parseFromString(htmlString, 'text/html');
// 2. Define allowed tags and attributes
const allowedTags = ['IMG', 'SPAN', 'B', 'I', 'BR', 'EM', 'STRONG'];
const allowedAttrs = ['src', 'class'];
// 3. Walk through all elements in the parsed document
const allElements = doc.body.querySelectorAll('*');
allElements.forEach(el => {
// Check if it's an <img> tag and enforce class restrictions
if (el.tagName === 'IMG') {
const className = el.getAttribute('class') || '';
// Split classes by whitespace in case there are multiple (e.g., class="emote custom")
const classes = className.split(/\s+/);
// Must contain either 'emote' or 'bits' to be allowed
const hasValidClass = classes.includes('emote') || classes.includes('bits');
if (!hasValidClass) {
// Unwrap or remove the unauthorized <img> tag
const parent = el.parentNode;
while (el.firstChild) {
parent.insertBefore(el.firstChild, el);
}
parent.removeChild(el);
return;
}
} }
// 3. SILENT WARNING: Minors match, but the installed patch version is lagging. // If any other tag isn't in our allowed list, unwrap it
if (iMinor === rMinor && iPatch < rPatch) { if (!allowedTags.includes(el.tagName)) {
console.log(`VersionCheck: Patch version mismatch. Required: ${rPatch}, Installed: ${iPatch}`); const parent = el.parentNode;
return 'compatible'; while (el.firstChild) {
parent.insertBefore(el.firstChild, el);
}
parent.removeChild(el);
return;
} }
// 4. PERFECT: Installed version meets or exceeds the required baseline. // Strip out any attributes not explicitly allowed (like onerror, onload, etc.)
console.log(`VersionCheck: Installed version meets or exceeds the required baseline. Required: ${requiredVersion}, Installed: ${installedVersion}`); Array.from(el.attributes).forEach(attr => {
return 'compatible'; if (!allowedAttrs.includes(attr.name.toLowerCase())) {
el.removeAttribute(attr.name);
}
});
});
return doc.body.innerHTML;
} }
+28 -12
View File
@@ -26,7 +26,11 @@ const line3 = document.getElementById('line3');
// OPTIONS // // OPTIONS //
///////////// /////////////
const language = urlParams.get("language") || "en";
const timezone = urlParams.get("timezone") || "";
const font = urlParams.get("font") || ""; const font = urlParams.get("font") || "";
const textAlignment = urlParams.get("textAlignment") || "center";
const enableLine1 = GetBooleanParam("enableLine1", true); const enableLine1 = GetBooleanParam("enableLine1", true);
const line1Format = urlParams.get("line1Format") || "hh:mm:ss A"; const line1Format = urlParams.get("line1Format") || "hh:mm:ss A";
@@ -35,7 +39,6 @@ const line1FontWeight = urlParams.get("line1FontWeight") || "700";
const line1FontColor = urlParams.get("line1FontColor") || "#ffffff"; const line1FontColor = urlParams.get("line1FontColor") || "#ffffff";
const line1FontOpacity = urlParams.get("line1FontOpacity") || "1"; const line1FontOpacity = urlParams.get("line1FontOpacity") || "1";
const line1TextTransform = urlParams.get("line1TextTransform") || "none"; const line1TextTransform = urlParams.get("line1TextTransform") || "none";
const line1TextAlignment = urlParams.get("line1TextAlignment") || "center";
const enableLine2 = GetBooleanParam("enableLine2", true); const enableLine2 = GetBooleanParam("enableLine2", true);
const line2Format = urlParams.get("line2Format") || "ddd, MMM D"; const line2Format = urlParams.get("line2Format") || "ddd, MMM D";
@@ -44,7 +47,6 @@ const line2FontWeight = urlParams.get("line2FontWeight") || "400";
const line2FontColor = urlParams.get("line2FontColor") || "#ffffff"; const line2FontColor = urlParams.get("line2FontColor") || "#ffffff";
const line2FontOpacity = urlParams.get("line2FontOpacity") || "0.7"; const line2FontOpacity = urlParams.get("line2FontOpacity") || "0.7";
const line2TextTransform = urlParams.get("line2TextTransform") || "none"; const line2TextTransform = urlParams.get("line2TextTransform") || "none";
const line2TextAlignment = urlParams.get("line2TextAlignment") || "center";
const enableLine3 = GetBooleanParam("enableLine3", false); const enableLine3 = GetBooleanParam("enableLine3", false);
const line3Format = urlParams.get("line3Format") || "ddd DD MMM YYYY hh:mm:ss A z"; const line3Format = urlParams.get("line3Format") || "ddd DD MMM YYYY hh:mm:ss A z";
@@ -53,7 +55,6 @@ const line3FontWeight = urlParams.get("line3FontWeight") || "600";
const line3FontColor = urlParams.get("line3FontColor") || "#ffffff"; const line3FontColor = urlParams.get("line3FontColor") || "#ffffff";
const line3FontOpacity = urlParams.get("line3FontOpacity") || "1"; const line3FontOpacity = urlParams.get("line3FontOpacity") || "1";
const line3TextTransform = urlParams.get("line3TextTransform") || "none"; const line3TextTransform = urlParams.get("line3TextTransform") || "none";
const line3TextAlignment = urlParams.get("line3TextAlignment") || "center";
@@ -75,10 +76,12 @@ if (!enableLine3)
//////////// ////////////////
// CLOCKO // // CLOCKO //
//////////// ////////////////
// Helper function to start the clock loop
function StartClock() {
// Check if any active format string includes milliseconds (e.g., 'S', 'SS', 'SSS') // Check if any active format string includes milliseconds (e.g., 'S', 'SS', 'SSS')
const usesMilliseconds = const usesMilliseconds =
(enableLine1 && line1Format.includes('S')) || (enableLine1 && line1Format.includes('S')) ||
@@ -88,32 +91,45 @@ const usesMilliseconds =
UpdateTime(); UpdateTime();
if (usesMilliseconds) { if (usesMilliseconds) {
// High-frequency updates for millisecond precision
function updateFrame() { function updateFrame() {
UpdateTime(); UpdateTime();
requestAnimationFrame(updateFrame); requestAnimationFrame(updateFrame);
} }
requestAnimationFrame(updateFrame); requestAnimationFrame(updateFrame);
} else { } else {
// Efficient 1-second interval for standard clocks
setInterval(UpdateTime, 1000); setInterval(UpdateTime, 1000);
} }
}
function UpdateTime() { function UpdateTime() {
const now = dayjs().tz(dayjs.tz.guess()); const activeTimezone = timezone ? timezone : dayjs.tz.guess();
const now = dayjs().tz(activeTimezone);
if (enableLine1) line1.textContent = now.format(line1Format); if (enableLine1) line1.textContent = now.format(line1Format);
if (enableLine2) line2.textContent = now.format(line2Format); if (enableLine2) line2.textContent = now.format(line2Format);
if (enableLine3) line3.textContent = now.format(line3Format); if (enableLine3) line3.textContent = now.format(line3Format);
} }
// Set the language and start the clock only after it's loaded
const script = document.createElement('script');
script.src = `https://cdn.jsdelivr.net/npm/dayjs@1/locale/${language}.js`;
script.onload = () => {
dayjs.locale(language);
StartClock();
};
script.onerror = () => {
console.warn(`Locale '${language}' failed to load. Falling back to English.`);
dayjs.locale('en');
StartClock();
};
document.head.appendChild(script);
///////////// /////////////
// STYLING // // STYLING //
///////////// /////////////
function ApplyStyling(el, fontSize, fontWeight, fontColor, fontOpacity, textTransform, textAlignment) { function ApplyStyling(el, fontSize, fontWeight, fontColor, fontOpacity, textTransform) {
el.style.fontSize = fontSize + "px"; el.style.fontSize = fontSize + "px";
el.style.fontWeight = fontWeight; el.style.fontWeight = fontWeight;
el.style.color = fontColor; el.style.color = fontColor;
@@ -122,6 +138,6 @@ function ApplyStyling(el, fontSize, fontWeight, fontColor, fontOpacity, textTran
el.style.textAlign = textAlignment; el.style.textAlign = textAlignment;
} }
ApplyStyling(line1, line1FontSize, line1FontWeight, line1FontColor, line1FontOpacity, line1TextTransform, line1TextAlignment); ApplyStyling(line1, line1FontSize, line1FontWeight, line1FontColor, line1FontOpacity, line1TextTransform);
ApplyStyling(line2, line2FontSize, line2FontWeight, line2FontColor, line2FontOpacity, line2TextTransform, line2TextAlignment); ApplyStyling(line2, line2FontSize, line2FontWeight, line2FontColor, line2FontOpacity, line2TextTransform);
ApplyStyling(line3, line3FontSize, line3FontWeight, line3FontColor, line3FontOpacity, line3TextTransform, line3TextAlignment); ApplyStyling(line3, line3FontSize, line3FontWeight, line3FontColor, line3FontOpacity, line3TextTransform);
+62 -42
View File
@@ -1,5 +1,54 @@
{ {
"settings": [ "settings": [
{
"id": "language",
"label": "Language",
"description": "",
"type": "select",
"options": [
{ "value": "en", "label": "English" },
{ "value": "ar", "label": "العربية (Arabic)" },
{ "value": "bg", "label": "Български (Bulgarian)" },
{ "value": "zh-cn", "label": "中文 (Chinese Simplified)" },
{ "value": "zh-tw", "label": "中文 (Chinese Traditional)" },
{ "value": "cs", "label": "Čeština (Czech)" },
{ "value": "da", "label": "Dansk (Danish)" },
{ "value": "nl", "label": "Nederlands (Dutch)" },
{ "value": "fi", "label": "Suomi (Finnish)" },
{ "value": "fr", "label": "Français (French)" },
{ "value": "de", "label": "Deutsch (German)" },
{ "value": "el", "label": "Ελληνικά (Greek)" },
{ "value": "he", "label": "עברית (Hebrew)" },
{ "value": "hi", "label": "हिन्दी (Hindi)" },
{ "value": "hu", "label": "Magyar (Hungarian)" },
{ "value": "id", "label": "Bahasa Indonesia (Indonesian)" },
{ "value": "it", "label": "Italiano (Italian)" },
{ "value": "ja", "label": "日本語 (Japanese)" },
{ "value": "ko", "label": "한국어 (Korean)" },
{ "value": "nb", "label": "Norsk bokmål (Norwegian Bokmål)" },
{ "value": "pl", "label": "Polski (Polish)" },
{ "value": "pt", "label": "Português (Portuguese)" },
{ "value": "ro", "label": "Română (Romanian)" },
{ "value": "ru", "label": "Русский (Russian)" },
{ "value": "sk", "label": "Slovenčina (Slovak)" },
{ "value": "es", "label": "Español (Spanish)" },
{ "value": "sv", "label": "Svenska (Swedish)" },
{ "value": "th", "label": "ไทย (Thai)" },
{ "value": "tr", "label": "Türkçe (Turkish)" },
{ "value": "uk", "label": "Українська (Ukrainian)" },
{ "value": "vi", "label": "Tiếng Việt (Vietnamese)" }
],
"defaultValue": "en",
"group": "General"
},
{
"id": "timezone",
"label": "Timezone",
"description": "Enter an IANA timezone (e.g., America/New_York, Europe/London). Leave blank to use local time.",
"type": "timezone",
"defaultValue": "",
"group": "General"
},
{ {
"id": "font", "id": "font",
"label": "Font", "label": "Font",
@@ -8,6 +57,19 @@
"defaultValue": "", "defaultValue": "",
"group": "Appearance" "group": "Appearance"
}, },
{
"id": "textAlignment",
"label": "Text Alignment",
"description": "",
"type": "select",
"options": [
{ "value": "left", "label": "Left" },
{ "value": "center", "label": "Center" },
{ "value": "right", "label": "Right" }
],
"defaultValue": "center",
"group": "Appearance"
},
{ {
"id": "enableLine1", "id": "enableLine1",
"label": "Enable Line 1", "label": "Enable Line 1",
@@ -92,20 +154,6 @@
"showIf": "enableLine1", "showIf": "enableLine1",
"group": "Appearance" "group": "Appearance"
}, },
{
"id": "line1TextAlignment",
"label": "Text Alignment",
"description": "",
"type": "select",
"options": [
{ "value": "left", "label": "Left" },
{ "value": "center", "label": "Center" },
{ "value": "right", "label": "Right" }
],
"defaultValue": "center",
"showIf": "enableLine1",
"group": "Appearance"
},
{ {
"id": "enableLine2", "id": "enableLine2",
"label": "Enable Line 2", "label": "Enable Line 2",
@@ -190,20 +238,6 @@
"showIf": "enableLine2", "showIf": "enableLine2",
"group": "Appearance" "group": "Appearance"
}, },
{
"id": "line2TextAlignment",
"label": "Text Alignment",
"description": "",
"type": "select",
"options": [
{ "value": "left", "label": "Left" },
{ "value": "center", "label": "Center" },
{ "value": "right", "label": "Right" }
],
"defaultValue": "center",
"showIf": "enableLine2",
"group": "Appearance"
},
{ {
"id": "enableLine3", "id": "enableLine3",
"label": "Enable Line 3", "label": "Enable Line 3",
@@ -287,20 +321,6 @@
"defaultValue": "none", "defaultValue": "none",
"showIf": "enableLine3", "showIf": "enableLine3",
"group": "Appearance" "group": "Appearance"
},
{
"id": "line3TextAlignment",
"label": "Text Alignment",
"description": "",
"type": "select",
"options": [
{ "value": "left", "label": "Left" },
{ "value": "center", "label": "Center" },
{ "value": "right", "label": "Right" }
],
"defaultValue": "center",
"showIf": "enableLine3",
"group": "Appearance"
} }
] ]
} }
+9 -4
View File
@@ -75,6 +75,7 @@ const showPatreonMemberships = GetBooleanParam("showPatreonMemberships", true);
const showKofiDonations = GetBooleanParam("showKofiDonations", true); const showKofiDonations = GetBooleanParam("showKofiDonations", true);
const showTipeeeStreamDonations = GetBooleanParam("showTipeeeStreamDonations", true); const showTipeeeStreamDonations = GetBooleanParam("showTipeeeStreamDonations", true);
const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", true); const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", true);
const skipFourthwallFreeOrders = GetBooleanParam("skipFourthwallFreeOrders", true);
const furryMode = GetBooleanParam("furryMode", false); const furryMode = GetBooleanParam("furryMode", false);
@@ -782,12 +783,12 @@ async function TwitchRewardRedemption(data) {
if (!showTwitchChannelPointRedemptions) if (!showTwitchChannelPointRedemptions)
return; return;
let username = data.user_name; let username = data.user_name ?? data.user.name;
if (data.user_name.toLowerCase() != data.user_login.toLowerCase()) if (username.toLowerCase() != (data.user_login ?? data.user.login).toLowerCase())
username = `${data.user_name} (${data.user_login})`; username = `${username} (${data.user_login ?? data.user.login})`;
const rewardName = data.reward.title; const rewardName = data.reward.title;
const cost = data.reward.cost; const cost = data.reward.cost;
const userInput = data.user_input; const userInput = data.user_input ?? data.userInput;
const channelPointIcon = `<img src="icons/badges/twitch-channel-point.png" class="platform"/>`; const channelPointIcon = `<img src="icons/badges/twitch-channel-point.png" class="platform"/>`;
let message = `${username} redeemed ${rewardName} ${channelPointIcon} ${cost}`; let message = `${username} redeemed ${rewardName} ${channelPointIcon} ${cost}`;
@@ -1198,6 +1199,10 @@ function FourthwallOrderPlaced(data) {
const item = data.variants[0].name; const item = data.variants[0].name;
const itemsOrdered = data.variants.length; const itemsOrdered = data.variants.length;
// Skip free orders if the user has chosen to do so
if (skipFourthwallFreeOrders && orderTotal == 0)
return;
let message = ""; let message = "";
// If there user did not provide a username, just say "Someone" // If there user did not provide a username, just say "Someone"
+9
View File
@@ -379,6 +379,15 @@
"defaultValue": true, "defaultValue": true,
"group": "Which donation messages do you want to see?" "group": "Which donation messages do you want to see?"
}, },
{
"id": "skipFourthwallFreeOrders",
"label": "Skip Free Orders",
"description": "If enabled, free orders from Fourthwall will be skipped.",
"type": "checkbox",
"defaultValue": true,
"showIf": "showFourthwallAlerts",
"group": "Which donation messages do you want to see?"
},
{ {
"id": "furryMode", "id": "furryMode",
"label": "Furry Mode", "label": "Furry Mode",
+11 -6
View File
@@ -82,6 +82,7 @@ const showPatreonMemberships = GetBooleanParam("showPatreonMemberships", true);
const showKofiDonations = GetBooleanParam("showKofiDonations", true); const showKofiDonations = GetBooleanParam("showKofiDonations", true);
const showTipeeeStreamDonations = GetBooleanParam("showTipeeeStreamDonations", true); const showTipeeeStreamDonations = GetBooleanParam("showTipeeeStreamDonations", true);
const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", true); const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", true);
const skipFourthwallFreeOrders = GetBooleanParam("skipFourthwallFreeOrders", true);
const furryMode = GetBooleanParam("furryMode", false); const furryMode = GetBooleanParam("furryMode", false);
@@ -90,7 +91,7 @@ const furryMode = GetBooleanParam("furryMode", false);
//////////////////// ////////////////////
const animationSpeed = GetIntParam("animationSpeed", 0.1); const animationSpeed = GetIntParam("animationSpeed", 0.1);
const randomYouTubeColors = GetBooleanParam("randomYouTubeColors", false); const randomYouTubeColors = GetBooleanParam("randomYouTubeColors", true);
const youtubeColor = urlParams.get("youtubeColor") || "#f70000"; const youtubeColor = urlParams.get("youtubeColor") || "#f70000";
const youtubeCustomSubIcon = urlParams.get("youtubeCustomSubIcon") || ""; const youtubeCustomSubIcon = urlParams.get("youtubeCustomSubIcon") || "";
let twitchUsername = urlParams.get("twitchUsername") || ""; let twitchUsername = urlParams.get("twitchUsername") || "";
@@ -1154,7 +1155,7 @@ async function TwitchRewardRedemption(data) {
if (showAvatar) { if (showAvatar) {
// Render avatars // Render avatars
const username = data.user_login; const username = data.user_login ?? data.user.login;
const avatarURL = await GetAvatar(username, 'twitch'); const avatarURL = await GetAvatar(username, 'twitch');
const avatar = new Image(); const avatar = new Image();
avatar.src = avatarURL; avatar.src = avatarURL;
@@ -1163,12 +1164,12 @@ async function TwitchRewardRedemption(data) {
} }
// Set the text // Set the text
let username = data.user_name; let username = data.user_name ?? data.user.name;
if (data.user_name.toLowerCase() != data.user_login.toLowerCase()) if (username.toLowerCase() != (data.user_login ?? data.user.login).toLowerCase())
username = `${data.user_name} (${data.user_login})`; username = `${username} (${data.user_login ?? data.user.login})`;
const rewardName = data.reward.title; const rewardName = data.reward.title;
const cost = data.reward.cost; const cost = data.reward.cost;
const userInput = data.user_input; const userInput = data.user_input ?? data.userInput;
const channelPointIcon = `<img src="icons/badges/twitch-channel-point.png" class="platform"/>`; const channelPointIcon = `<img src="icons/badges/twitch-channel-point.png" class="platform"/>`;
titleDiv.innerHTML = `${username} redeemed ${rewardName} ${channelPointIcon} ${cost}`; titleDiv.innerHTML = `${username} redeemed ${rewardName} ${channelPointIcon} ${cost}`;
@@ -2045,6 +2046,10 @@ function FourthwallOrderPlaced(data) {
const itemImageUrl = data.variants[0].image; const itemImageUrl = data.variants[0].image;
const fourthwallProductImage = `<img src="${itemImageUrl}" class="productImage"/>`; const fourthwallProductImage = `<img src="${itemImageUrl}" class="productImage"/>`;
// Skip free orders if the user has chosen to do so
if (skipFourthwallFreeOrders && orderTotal == 0)
return;
avatarDiv.innerHTML = fourthwallProductImage; avatarDiv.innerHTML = fourthwallProductImage;
let contents = ""; let contents = "";
+9
View File
@@ -517,6 +517,15 @@
"defaultValue": true, "defaultValue": true,
"group": "Which donation messages do you want to see?" "group": "Which donation messages do you want to see?"
}, },
{
"id": "skipFourthwallFreeOrders",
"label": "Skip Free Orders",
"description": "If enabled, free orders from Fourthwall will be skipped.",
"type": "checkbox",
"defaultValue": true,
"showIf": "showFourthwallAlerts",
"group": "Which donation messages do you want to see?"
},
{ {
"id": "furryMode", "id": "furryMode",
"label": "Furry Mode", "label": "Furry Mode",
+8 -3
View File
@@ -112,6 +112,7 @@ const showTipeeeStreamDonations = GetBooleanParam("showTipeeeStreamDonations", f
const tipeeestreamDonationAction = urlParams.get("tipeeestreamDonationAction") || ""; const tipeeestreamDonationAction = urlParams.get("tipeeestreamDonationAction") || "";
const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", false); const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", false);
const fourthwallAlertAction = urlParams.get("fourthwallAlertAction") || ""; const fourthwallAlertAction = urlParams.get("fourthwallAlertAction") || "";
const skipFourthwallFreeOrders = GetBooleanParam("skipFourthwallFreeOrders", true);
//////////////////// ////////////////////
// HIDDEN OPTIONS // // HIDDEN OPTIONS //
@@ -687,14 +688,14 @@ async function TwitchRewardRedemption(data) {
if (!showTwitchChannelPointRedemptions) if (!showTwitchChannelPointRedemptions)
return; return;
const username = data.user_name; const username = data.user_name ?? data.user.name;
const rewardName = data.reward.title; const rewardName = data.reward.title;
const cost = data.reward.cost; const cost = data.reward.cost;
const userInput = data.user_input; const userInput = data.user_input ?? data.userInput;
const channelPointIcon = `<img src="icons/badges/twitch-channel-point.png" class="platform" style="height: 1em"/>`; const channelPointIcon = `<img src="icons/badges/twitch-channel-point.png" class="platform" style="height: 1em"/>`;
// Render avatars // Render avatars
const avatarURL = await GetAvatar(data.user_login, 'twitch'); const avatarURL = await GetAvatar(data.user_login ?? data.user.login, 'twitch');
UpdateAlertBox( UpdateAlertBox(
'twitch', 'twitch',
@@ -1124,6 +1125,10 @@ function FourthwallOrderPlaced(data) {
const message = DecodeHTMLString(data.statmessageus); const message = DecodeHTMLString(data.statmessageus);
const itemImageUrl = data.variants[0].image; const itemImageUrl = data.variants[0].image;
// Skip free orders if the user has chosen to do so
if (skipFourthwallFreeOrders && orderTotal == 0)
return;
// If there user did not provide a username, just say "Someone" // If there user did not provide a username, just say "Someone"
if (user == undefined) if (user == undefined)
user = "Someone"; user = "Someone";
@@ -628,6 +628,15 @@
"showIf": "showFourthwallAlerts", "showIf": "showFourthwallAlerts",
"group": "Which donation alerts do you want to see?" "group": "Which donation alerts do you want to see?"
}, },
{
"id": "skipFourthwallFreeOrders",
"label": "Skip Free Orders",
"description": "If enabled, free orders from Fourthwall will be skipped.",
"type": "checkbox",
"defaultValue": true,
"showIf": "showFourthwallAlerts",
"group": "Which donation alerts do you want to see?"
},
{ {
"id": "address", "id": "address",
"label": "IP Address", "label": "IP Address",
+8 -7
View File
@@ -9,7 +9,7 @@ const urlParams = new URLSearchParams(queryString);
// CONSTANTS // // CONSTANTS //
//////////////// ////////////////
const REQUIRED_VERSION = '0.0.4'; const REQUIRED_VERSION = '1.0.0';
const SMTC_BRIDGE_DOWNLOAD_URL = 'https://github.com/nuttylmao/smtc-bridge/releases'; const SMTC_BRIDGE_DOWNLOAD_URL = 'https://github.com/nuttylmao/smtc-bridge/releases';
let VersionChecked = false; let VersionChecked = false;
@@ -36,6 +36,8 @@ const useCustomColors = GetBooleanParam("useCustomColors", false);
const color1 = urlParams.get('color1') || '#ffffff'; const color1 = urlParams.get('color1') || '#ffffff';
const color2 = urlParams.get('color2') || '#1d1d1d'; const color2 = urlParams.get('color2') || '#1d1d1d';
const autoHide = GetBooleanParam("autoHide", false);
const showWhilePaused = GetBooleanParam("showWhilePaused", false);
const includedApplications = urlParams.get('includedApplications') || ''; const includedApplications = urlParams.get('includedApplications') || '';
const excludedApplications = urlParams.get('excludedApplications') || ''; const excludedApplications = urlParams.get('excludedApplications') || '';
const showAlbumArt = GetBooleanParam("showAlbumArt", true); const showAlbumArt = GetBooleanParam("showAlbumArt", true);
@@ -43,7 +45,6 @@ const showProgressBar = GetBooleanParam("showProgressBar", true);
const swapArtistTrack = GetBooleanParam("swapArtistTrack", false); const swapArtistTrack = GetBooleanParam("swapArtistTrack", false);
const showPrimary = GetBooleanParam("showPrimary", true); const showPrimary = GetBooleanParam("showPrimary", true);
const showSecondary = GetBooleanParam("showSecondary", true); const showSecondary = GetBooleanParam("showSecondary", true);
const autoHide = GetBooleanParam("autoHide", false);
const displayDuration = GetIntParam("displayDuration", 5); const displayDuration = GetIntParam("displayDuration", 5);
const showAnimation = urlParams.get('showAnimation') || 'slide-in-from-bottom'; const showAnimation = urlParams.get('showAnimation') || 'slide-in-from-bottom';
const hideAnimation = urlParams.get('hideAnimation') || 'slide-out-bottom'; const hideAnimation = urlParams.get('hideAnimation') || 'slide-out-bottom';
@@ -363,7 +364,7 @@ async function UpdatePlayerState(data) {
for (const targetApp of includedList) { for (const targetApp of includedList) {
targetSession = validSessions.find(s => { targetSession = validSessions.find(s => {
const matchesApp = (s.source_app_id || "").toLowerCase().includes(targetApp); const matchesApp = (s.source_app_id || "").toLowerCase().includes(targetApp);
const isPlaying = s.playback_info && s.playback_info.PlaybackStatus === PlaybackStatus.PLAYING; const isPlaying = s.playback_info && (s.playback_info.PlaybackStatus === PlaybackStatus.PLAYING || (showWhilePaused && s.playback_info.PlaybackStatus === PlaybackStatus.PAUSED));
return matchesApp && isPlaying; return matchesApp && isPlaying;
}); });
if (targetSession) break; if (targetSession) break;
@@ -387,7 +388,7 @@ async function UpdatePlayerState(data) {
// Priority 2: If the current session isn't available/valid, find any session that is currently playing // Priority 2: If the current session isn't available/valid, find any session that is currently playing
if (!targetSession || targetSession.playback_info.PlaybackStatus !== PlaybackStatus.PLAYING) { if (!targetSession || targetSession.playback_info.PlaybackStatus !== PlaybackStatus.PLAYING) {
const playingSession = validSessions.find(s => s.playback_info && s.playback_info.PlaybackStatus === PlaybackStatus.PLAYING); const playingSession = validSessions.find(s => s.playback_info && (s.playback_info.PlaybackStatus === PlaybackStatus.PLAYING || (showWhilePaused && s.playback_info.PlaybackStatus === PlaybackStatus.PAUSED)));
if (playingSession) { if (playingSession) {
targetSession = playingSession; targetSession = playingSession;
} }
@@ -411,7 +412,7 @@ async function UpdatePlayerState(data) {
// 1. Check if playback status has changed and update visibility accordingly // 1. Check if playback status has changed and update visibility accordingly
if (playbackInfo.PlaybackStatus !== CurrentPlaybackStatus) { if (playbackInfo.PlaybackStatus !== CurrentPlaybackStatus) {
if (playbackInfo.PlaybackStatus === PlaybackStatus.PLAYING) if (playbackInfo.PlaybackStatus === PlaybackStatus.PLAYING || (showWhilePaused && playbackInfo.PlaybackStatus === PlaybackStatus.PAUSED))
SetVisibility(true); SetVisibility(true);
else else
SetVisibility(false); SetVisibility(false);
@@ -421,7 +422,7 @@ async function UpdatePlayerState(data) {
// 2. Check if the track name/artist have changed - this is our indicator that the next track has loaded // 2. Check if the track name/artist have changed - this is our indicator that the next track has loaded
// Only proceed if the player state is actively playing audio // Only proceed if the player state is actively playing audio
if (CurrentPlaybackStatus == PlaybackStatus.PLAYING) { if (CurrentPlaybackStatus == PlaybackStatus.PLAYING || (showWhilePaused && CurrentPlaybackStatus == PlaybackStatus.PAUSED)) {
const newTrackKey = `${mediaProps.Title}-${mediaProps.Artist}-${mediaProps.Thumbnail}`; const newTrackKey = `${mediaProps.Title}-${mediaProps.Artist}-${mediaProps.Thumbnail}`;
if (newTrackKey !== CurrentSongKey) { if (newTrackKey !== CurrentSongKey) {
ChangeTrack(mediaProps, accentColorPalette); // Now trigger your cross-fade logic here! ChangeTrack(mediaProps, accentColorPalette); // Now trigger your cross-fade logic here!
@@ -442,7 +443,7 @@ async function UpdatePlayerState(data) {
const isPlaying = (targetSession.playback_info.PlaybackStatus === PlaybackStatus.PLAYING); const isPlaying = (targetSession.playback_info.PlaybackStatus === PlaybackStatus.PLAYING);
const currentPositionMs = isPlaying && timelineProps.EndTime > 0 ? timelineProps.Position + driftMs : timelineProps.Position; const currentPositionMs = isPlaying && timelineProps.EndTime > 0 ? timelineProps.Position + driftMs : timelineProps.Position;
SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette); SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, targetSession.playback_info.PlaybackStatus);
} }
} }
else { else {
+27 -19
View File
@@ -150,6 +150,33 @@
"showIf": "useCustomColors", "showIf": "useCustomColors",
"group": "Appearance" "group": "Appearance"
}, },
{
"id": "showWhilePaused",
"label": "Show While Paused",
"description": "If enabled, the widget will be visible even when the track is paused.",
"type": "checkbox",
"defaultValue": false,
"group": "General"
},
{
"id": "autoHide",
"label": "Auto-Hide on Track Change",
"description": "If enabled, the widget will show for a set duration whenever a new track starts.",
"type": "checkbox",
"defaultValue": false,
"group": "General"
},
{
"id": "displayDuration",
"label": "Display Duration (seconds)",
"description": "How long the widget should remain visible.",
"type": "number",
"min": 1,
"max": 60,
"defaultValue": 5,
"showIf": "autoHide",
"group": "General"
},
{ {
"id": "includedApplications", "id": "includedApplications",
"label": "Included Apps", "label": "Included Apps",
@@ -208,25 +235,6 @@
"defaultValue": true, "defaultValue": true,
"group": "General" "group": "General"
}, },
{
"id": "autoHide",
"label": "Auto-Hide on Track Change",
"description": "If enabled, the widget will show for a set duration whenever a new track starts.",
"type": "checkbox",
"defaultValue": false,
"group": "General"
},
{
"id": "displayDuration",
"label": "Display Duration (seconds)",
"description": "How long the widget should remain visible.",
"type": "number",
"min": 1,
"max": 60,
"defaultValue": 5,
"showIf": "autoHide",
"group": "General"
},
{ {
"id": "showAnimation", "id": "showAnimation",
"label": "Show Animation", "label": "Show Animation",
+4 -4
View File
@@ -97,7 +97,7 @@ body {
@keyframes slide-in-from-left { @keyframes slide-in-from-left {
0% { 0% {
transform: translateX(-1em); transform: translateX(-100%);
opacity: 0; opacity: 0;
} }
@@ -114,14 +114,14 @@ body {
} }
100% { 100% {
transform: translateX(-1em); transform: translateX(-100%);
opacity: 0; opacity: 0;
} }
} }
@keyframes slide-in-from-right { @keyframes slide-in-from-right {
0% { 0% {
transform: translateX(1em); transform: translateX(100%);
opacity: 0; opacity: 0;
} }
@@ -138,7 +138,7 @@ body {
} }
100% { 100% {
transform: translateX(1em); transform: translateX(100%);
opacity: 0; opacity: 0;
} }
} }
+5
View File
@@ -3,6 +3,11 @@
<div id="album-art-container"> <div id="album-art-container">
<div id="album-art-layer"></div> <div id="album-art-layer"></div>
<div id="album-art-transition-layer"></div> <div id="album-art-transition-layer"></div>
<div id="pause-overlay">
<svg viewBox="0 0 24 24" style="fill: var(--accent-color, #ffffff);">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
</svg>
</div>
</div> </div>
<div id="text-info-container"> <div id="text-info-container">
+8 -3
View File
@@ -96,7 +96,12 @@ async function ChangeTrack(mediaProps, accentColorPalette) {
}, 250); }, 250);
} }
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) {
// Set the pause overlay visibility based on playback status
if (playbackStatus === PlaybackStatus.PAUSED)
albumArtContainer.classList.add('is-paused');
else
albumArtContainer.classList.remove('is-paused');
// Set progressbar // Set progressbar
// Ensure we don't divide by zero or exceed 100% // Ensure we don't divide by zero or exceed 100%
@@ -105,7 +110,7 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
progressPercent = Math.min(100, Math.max(0, progressPercent)); progressPercent = Math.min(100, Math.max(0, progressPercent));
progressBar.style.width = `${progressPercent}%`; progressBar.style.width = `${progressPercent}%`;
if (!useCustomColors) if (!useCustomColors)
progressBar.style.setProperty('--accent-color', accentColorPalette.LightVibrant); document.body.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
else else
progressBar.style.setProperty('--accent-color', color1); document.body.style.setProperty('--accent-color', color1);
} }
+30
View File
@@ -21,6 +21,36 @@
display: var(--show-album-art); display: var(--show-album-art);
} }
#album-art-container.is-paused #album-art-layer,
#album-art-container.is-paused #album-art-transition-layer {
opacity: 0.7;
}
#pause-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 10;
border-radius: inherit;
}
#pause-overlay svg {
height: 30%;
width: auto;
}
#album-art-container.is-paused #pause-overlay {
opacity: 1;
}
#album-art-layer, #album-art-layer,
#album-art-transition-layer { #album-art-transition-layer {
position: absolute; position: absolute;
+5
View File
@@ -9,6 +9,11 @@
<div id="album-art-container"> <div id="album-art-container">
<div id="album-art-layer"></div> <div id="album-art-layer"></div>
<div id="album-art-transition-layer"></div> <div id="album-art-transition-layer"></div>
<div id="pause-overlay">
<svg viewBox="0 0 24 24" style="fill: var(--accent-color, #ffffff);">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
</svg>
</div>
</div> </div>
<div id="track-label" class="text-label">&nbsp;</div> <div id="track-label" class="text-label">&nbsp;</div>
+7 -1
View File
@@ -84,7 +84,7 @@ async function ChangeTrack(mediaProps, accentColorPalette) {
}, 250); }, 250);
} }
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) {
// Update the label using your naming convention // Update the label using your naming convention
currentTimeLabel.innerText = currentTimeLabel.innerText =
ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs); ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs);
@@ -92,6 +92,12 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
durationLabel.innerText = durationLabel.innerText =
ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime); ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime);
// Set the pause overlay visibility based on playback status
if (playbackStatus === PlaybackStatus.PAUSED)
albumArtContainer.classList.add('is-paused');
else
albumArtContainer.classList.remove('is-paused');
// Set progressbar // Set progressbar
// Ensure we don't divide by zero or exceed 100% // Ensure we don't divide by zero or exceed 100%
const durationMs = timelineProps.EndTime; const durationMs = timelineProps.EndTime;
+30
View File
@@ -23,6 +23,36 @@
display: var(--show-album-art); display: var(--show-album-art);
} }
#album-art-container.is-paused #album-art-layer,
#album-art-container.is-paused #album-art-transition-layer {
opacity: 0.2;
}
#pause-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 10;
border-radius: inherit;
}
#pause-overlay svg {
height: 30%;
width: auto;
}
#album-art-container.is-paused #pause-overlay {
opacity: 0.8;
}
#album-art-layer, #album-art-layer,
#album-art-transition-layer { #album-art-transition-layer {
position: absolute; position: absolute;
+5
View File
@@ -3,6 +3,11 @@
<div id="album-art-container"> <div id="album-art-container">
<div id="album-art-layer"></div> <div id="album-art-layer"></div>
<div id="album-art-transition-layer"></div> <div id="album-art-transition-layer"></div>
<div id="pause-overlay">
<svg viewBox="0 0 24 24" style="fill: var(--accent-color, #ffffff);">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
</svg>
</div>
</div> </div>
<div id="song-info-container"> <div id="song-info-container">
+7 -1
View File
@@ -84,7 +84,7 @@ async function ChangeTrack(mediaProps, accentColorPalette) {
}, 250); }, 250);
} }
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) {
// Update the label using your naming convention // Update the label using your naming convention
currentTimeLabel.innerText = currentTimeLabel.innerText =
ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs); ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs);
@@ -92,6 +92,12 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
durationLabel.innerText = durationLabel.innerText =
ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime); ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime);
// Set the pause overlay visibility based on playback status
if (playbackStatus === PlaybackStatus.PAUSED)
albumArtContainer.classList.add('is-paused');
else
albumArtContainer.classList.remove('is-paused');
// Set progressbar // Set progressbar
// Ensure we don't divide by zero or exceed 100% // Ensure we don't divide by zero or exceed 100%
const durationMs = timelineProps.EndTime; const durationMs = timelineProps.EndTime;
+31
View File
@@ -24,6 +24,37 @@
flex-shrink: 0; flex-shrink: 0;
} }
#album-art-container.is-paused #album-art-layer,
#album-art-container.is-paused #album-art-transition-layer {
opacity: 0.7;
filter: grayscale(50%);
}
#pause-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 10;
border-radius: inherit;
}
#pause-overlay svg {
height: 40%;
width: auto;
}
#album-art-container.is-paused #pause-overlay {
opacity: 1;
}
#album-art-layer, #album-art-layer,
#album-art-transition-layer { #album-art-transition-layer {
position: absolute; position: absolute;
+1 -1
View File
@@ -94,7 +94,7 @@ async function ChangeTrack(mediaProps, accentColorPalette) {
}, 250); }, 250);
} }
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) {
const durationMs = timelineProps.EndTime; const durationMs = timelineProps.EndTime;
let progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0; let progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0;
+7 -1
View File
@@ -97,7 +97,7 @@ async function ChangeTrack(mediaProps, accentColorPalette) {
}, 250); }, 250);
} }
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) {
// Set progressbar // Set progressbar
// Ensure we don't divide by zero or exceed 100% // Ensure we don't divide by zero or exceed 100%
const durationMs = timelineProps.EndTime; const durationMs = timelineProps.EndTime;
@@ -122,6 +122,12 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
// const transitionWidth = '1em'; // const transitionWidth = '1em';
// progressBar.style.maskImage = `linear-gradient(to right, black calc(${clipRight}% - ${transitionWidth}), transparent ${clipRight}%)` // progressBar.style.maskImage = `linear-gradient(to right, black calc(${clipRight}% - ${transitionWidth}), transparent ${clipRight}%)`
// progressBarTrack.style.maskImage = `linear-gradient(to right, transparent calc(${clipRight}% - ${transitionWidth}), black ${clipRight}%)`; // progressBarTrack.style.maskImage = `linear-gradient(to right, transparent calc(${clipRight}% - ${transitionWidth}), black ${clipRight}%)`;
// Set the pause overlay visibility based on playback status
if (playbackStatus === PlaybackStatus.PAUSED)
songInfoContainer.classList.add('is-paused');
else
songInfoContainer.classList.remove('is-paused');
} }
// MARQUEE LOGIC // MARQUEE LOGIC
+4
View File
@@ -24,6 +24,10 @@
grid-template-areas: "stack"; grid-template-areas: "stack";
} }
#song-info-container.is-paused {
opacity: 0.8;
}
/* The content sits on top */ /* The content sits on top */
#progress-bar { #progress-bar {
position: relative; position: relative;
+5
View File
@@ -2,6 +2,11 @@
<div id="album-art-container"> <div id="album-art-container">
<div id="album-art-layer"></div> <div id="album-art-layer"></div>
<div id="album-art-transition-layer"></div> <div id="album-art-transition-layer"></div>
<div id="pause-overlay">
<svg viewBox="0 0 24 24" style="fill: var(--accent-color, #ffffff);">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
</svg>
</div>
</div> </div>
<div id="song-info-container"> <div id="song-info-container">
+9 -3
View File
@@ -78,7 +78,7 @@ async function ChangeTrack(mediaProps) {
}, 250); }, 250);
} }
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) {
// Update the label using your naming convention // Update the label using your naming convention
currentTimeLabel.innerText = currentTimeLabel.innerText =
ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs); ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs);
@@ -86,6 +86,12 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
durationLabel.innerText = durationLabel.innerText =
ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime); ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime);
// Set the pause overlay visibility based on playback status
if (playbackStatus === PlaybackStatus.PAUSED)
albumArtContainer.classList.add('is-paused');
else
albumArtContainer.classList.remove('is-paused');
// Set progressbar // Set progressbar
// Ensure we don't divide by zero or exceed 100% // Ensure we don't divide by zero or exceed 100%
const durationMs = timelineProps.EndTime; const durationMs = timelineProps.EndTime;
@@ -95,12 +101,12 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
if (!useCustomColors) if (!useCustomColors)
{ {
progressBarFill.style.setProperty('--accent-color', accentColorPalette.LightVibrant); document.body.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
document.body.style.color = accentColorPalette.LightVibrant; document.body.style.color = accentColorPalette.LightVibrant;
} }
else else
{ {
progressBarFill.style.setProperty('--accent-color', color1); document.body.style.setProperty('--accent-color', color1);
document.body.style.color = color1; document.body.style.color = color1;
} }
} }
+31
View File
@@ -23,6 +23,37 @@
flex-shrink: 0; flex-shrink: 0;
} }
#album-art-container.is-paused #album-art-layer,
#album-art-container.is-paused #album-art-transition-layer {
opacity: 0.7;
filter: grayscale(50%);
}
#pause-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 10;
border-radius: inherit;
}
#pause-overlay svg {
height: 40%;
width: auto;
}
#album-art-container.is-paused #pause-overlay {
opacity: 1;
}
#album-art-layer, #album-art-layer,
#album-art-transition-layer { #album-art-transition-layer {
position: absolute; position: absolute;
+5
View File
@@ -6,6 +6,11 @@
<div id="album-art-container"> <div id="album-art-container">
<div id="album-art-layer"></div> <div id="album-art-layer"></div>
<div id="album-art-transition-layer"></div> <div id="album-art-transition-layer"></div>
<div id="pause-overlay">
<svg viewBox="0 0 24 24" style="fill: var(--accent-color, #ffffff);">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
</svg>
</div>
</div> </div>
<div id="song-info-wrapper"> <div id="song-info-wrapper">
+12 -4
View File
@@ -147,7 +147,7 @@ async function ChangeTrack(mediaProps, accentColorPalette) {
}, 250); }, 250);
} }
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) {
// Update the label using your naming convention // Update the label using your naming convention
currentTimeLabel.innerText = currentTimeLabel.innerText =
ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs); ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs);
@@ -155,6 +155,12 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
durationLabel.innerText = durationLabel.innerText =
ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime); ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime);
// Set the pause overlay visibility based on playback status
if (playbackStatus === PlaybackStatus.PAUSED)
albumArtContainer.classList.add('is-paused');
else
albumArtContainer.classList.remove('is-paused');
// Set progressbar // Set progressbar
// Ensure we don't divide by zero or exceed 100% // Ensure we don't divide by zero or exceed 100%
const durationMs = timelineProps.EndTime; const durationMs = timelineProps.EndTime;
@@ -165,9 +171,11 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
if (!useCustomColors) { if (!useCustomColors) {
switch (themeVariant) { switch (themeVariant) {
case "matte": case "matte":
progressBarFill.style.setProperty('--accent-color', accentColorPalette.DarkVibrant); document.body.style.setProperty('--accent-color', accentColorPalette.DarkVibrant);
break; break;
case "matte-dark": case "matte-dark":
document.body.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
break;
default: default:
progressBarFill.style.setProperty('--accent-color', accentColorPalette.LightVibrant); progressBarFill.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
break; break;
@@ -177,11 +185,11 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
{ {
switch (themeVariant) { switch (themeVariant) {
case "matte": case "matte":
progressBarFill.style.setProperty('--accent-color', color2); document.body.style.setProperty('--accent-color', color2);
break; break;
// case "matte-dark": // case "matte-dark":
default: default:
progressBarFill.style.setProperty('--accent-color', color1); document.body.style.setProperty('--accent-color', color1);
break; break;
} }
} }
+30
View File
@@ -25,6 +25,36 @@
flex-shrink: 0; flex-shrink: 0;
} }
#album-art-container.is-paused #album-art-layer,
#album-art-container.is-paused #album-art-transition-layer {
opacity: 0.2;
}
#pause-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 10;
border-radius: inherit;
}
#pause-overlay svg {
height: 50%;
width: auto;
}
#album-art-container.is-paused #pause-overlay {
opacity: 1;
}
#album-art-layer, #album-art-layer,
#album-art-transition-layer { #album-art-transition-layer {
position: absolute; position: absolute;
+50 -1
View File
@@ -8,6 +8,7 @@ const albumArtContainer = document.getElementById('album-art-container');
const albumArtLayer = document.getElementById('album-art-layer'); const albumArtLayer = document.getElementById('album-art-layer');
const albumArtTransition = document.getElementById('album-art-transition-layer'); const albumArtTransition = document.getElementById('album-art-transition-layer');
const progressCircle = document.getElementById('progress-pie-circle'); const progressCircle = document.getElementById('progress-pie-circle');
const vinylContainer = document.getElementById('vinyl-container');
/////////////// ///////////////
// CONSTANTS // // CONSTANTS //
@@ -60,7 +61,7 @@ async function ChangeTrack(mediaProps) {
}, 250); }, 250);
} }
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) {
// Set progressbar // Set progressbar
// Ensure we don't divide by zero or exceed 100% // Ensure we don't divide by zero or exceed 100%
const durationMs = timelineProps.EndTime; const durationMs = timelineProps.EndTime;
@@ -75,4 +76,52 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
// 0% progress = 157.1 offset. 100% progress = 0 offset. // 0% progress = 157.1 offset. 100% progress = 0 offset.
const offset = circumference - (progressPercent / 100) * circumference; const offset = circumference - (progressPercent / 100) * circumference;
progressCircle.style.strokeDashoffset = offset; progressCircle.style.strokeDashoffset = offset;
// Adjust 'Paused' check to match whatever string/value your backend sends
if (playbackStatus === PlaybackStatus.PAUSED)
setVinylPaused(true);
else
setVinylPaused(false);
}
const vinylAnimation = vinylContainer.animate(
[
{ transform: "rotate(0deg)" },
{ transform: "rotate(360deg)" }
],
{
duration: 10000,
iterations: Infinity,
easing: "linear"
}
);
let animationFrame;
function setVinylPaused(shouldPause) {
cancelAnimationFrame(animationFrame);
const from = vinylAnimation.playbackRate;
const to = shouldPause ? 0 : 1;
const duration = 700;
const start = performance.now();
vinylAnimation.play();
function update(now) {
const progress = Math.min((now - start) / duration, 1);
// Smooth acceleration/deceleration
const eased = progress * progress * (3 - 2 * progress);
vinylAnimation.playbackRate = from + (to - from) * eased;
if (progress < 1) {
animationFrame = requestAnimationFrame(update);
} else if (shouldPause) {
vinylAnimation.pause();
}
}
animationFrame = requestAnimationFrame(update);
} }
+61 -4
View File
@@ -16,7 +16,7 @@
justify-content: center; justify-content: center;
width: 100%; width: 100%;
filter: drop-shadow(0px 10px 15px rgba(0, 0, 0, 0.5)); filter: drop-shadow(0px 10px 15px rgba(0, 0, 0, 0.5));
animation: spin 10s linear infinite; transition: transform 1.2s cubic-bezier(0.25, 1, 0.5, 1);
} }
#vinyl-background { #vinyl-background {
@@ -91,7 +91,64 @@
transition: all 1s linear; transition: all 1s linear;
} }
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); } @keyframes slide-in-from-top {
0% {
transform: translateY(-100%);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slide-in-from-top {
0% {
transform: translateY(-100%);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slide-out-top {
0% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(-100%);
opacity: 0;
}
}
@keyframes slide-in-from-bottom {
0% {
transform: translateY(100%);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slide-out-bottom {
0% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(100%);
opacity: 0;
}
} }
+8 -8
View File
@@ -51,7 +51,7 @@ async function CustomEvent(data) {
subtitleEl.innerText = `${data.user}`; subtitleEl.innerText = `${data.user}`;
const messageEl = document.createElement('div'); const messageEl = document.createElement('div');
messageEl.innerHTML = data.message; messageEl.innerHTML = SanitizeHTML(data.message);
// Render emotes // Render emotes
for (i in data.emotes) { for (i in data.emotes) {
@@ -116,7 +116,7 @@ async function CustomEvent(data) {
const messageEl = document.createElement('div'); const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.cumulative} months${data.monthStreak > 1 ? ' (' + data.monthStreak + ' in a row!)' : ''}</b>`; messageEl.innerHTML = `<b>${data.cumulative} months${data.monthStreak > 1 ? ' (' + data.monthStreak + ' in a row!)' : ''}</b>`;
if (data.messageStripped) if (data.messageStripped)
messageEl.innerHTML += `<br><br><i>${data.messageStripped}</i>`; messageEl.innerHTML += `<br><br><i>${SanitizeHTML(data.messageStripped)}</i>`;
contentEl.appendChild(messageEl); contentEl.appendChild(messageEl);
@@ -196,7 +196,7 @@ async function CustomEvent(data) {
subtitleEl.innerText = `${data.user}`; subtitleEl.innerText = `${data.user}`;
const messageEl = document.createElement('div'); const messageEl = document.createElement('div');
messageEl.innerHTML = data.rawInput; messageEl.innerHTML = SanitizeHTML(data.rawInput);
// // Render emotes // // Render emotes
// for (i in data.emotes) { // for (i in data.emotes) {
@@ -284,7 +284,7 @@ async function CustomEvent(data) {
const messageEl = document.createElement('div'); const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.user}</b><br>sent a Super Chat!`; messageEl.innerHTML = `<b>${data.user}</b><br>sent a Super Chat!`;
if (data.message) if (data.message)
messageEl.innerHTML += `<br><br><i>${data.message}</i>`; messageEl.innerHTML += `<br><br><i>${SanitizeHTML(data.message)}</i>`;
contentEl.appendChild(messageEl); contentEl.appendChild(messageEl);
@@ -424,7 +424,7 @@ async function CustomEvent(data) {
if (data.tipMessage) { if (data.tipMessage) {
const messageEl = document.createElement('div'); const messageEl = document.createElement('div');
messageEl.innerHTML = `<i>${data.tipMessage}</i>`; messageEl.innerHTML = `<i>${SanitizeHTML(data.tipMessage)}</i>`;
contentEl.appendChild(messageEl); contentEl.appendChild(messageEl);
} }
@@ -448,7 +448,7 @@ async function CustomEvent(data) {
if (data.donationMessage) { if (data.donationMessage) {
const messageEl = document.createElement('div'); const messageEl = document.createElement('div');
messageEl.innerHTML = `<i>${data.donationMessage}</i>`; messageEl.innerHTML = `<i>${SanitizeHTML(data.donationMessage)}</i>`;
contentEl.appendChild(messageEl); contentEl.appendChild(messageEl);
} }
@@ -475,7 +475,7 @@ async function CustomEvent(data) {
if (data["fw.message"]) { if (data["fw.message"]) {
const messageEl = document.createElement('div'); const messageEl = document.createElement('div');
messageEl.innerHTML = `<i>${data["fw.message"]}</i>`; messageEl.innerHTML = `<i>${SanitizeHTML(data["fw.message"])}</i>`;
contentEl.appendChild(messageEl); contentEl.appendChild(messageEl);
} }
@@ -537,7 +537,7 @@ async function CustomEvent(data) {
if (customMessage) { if (customMessage) {
const txt = document.createElement("textarea"); const txt = document.createElement("textarea");
txt.innerHTML = customMessage; txt.innerHTML = customMessage;
customMessageEl.innerHTML += `<br><i>${txt.value}</i>`; customMessageEl.innerHTML += `<br><i>${SanitizeHTML(txt.value)}</i>`;
} }
// Add a cute thank you message because you're uwu like that // Add a cute thank you message because you're uwu like that
+1 -1
View File
@@ -146,7 +146,7 @@ client.on('Twitch.UpcomingAd', (response) => {
///////////////////// /////////////////////
function TwitchAdRun(data) { function TwitchAdRun(data) {
const duration = data.length_seconds; const duration = data.lengthSeconds ?? data.length_seconds;
// Unset the upcoming ad countdown warning // Unset the upcoming ad countdown warning
upcomingAdWarningStartDelay = null; upcomingAdWarningStartDelay = null;