mirror of
https://github.com/nuttylmao/nutty.gg.git
synced 2026-09-18 19:50:58 -04:00
@@ -188,6 +188,21 @@ function LoadJSON(settingsJson) {
|
||||
await PopulateFontDatalist();
|
||||
}, { once: true });
|
||||
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':
|
||||
inputElement = document.createElement('button');
|
||||
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
|
||||
PopulateFontDatalist();
|
||||
|
||||
// Populate timezones for auto-suggest
|
||||
PopulateTimezoneDatalist();
|
||||
|
||||
+62
-18
@@ -688,25 +688,69 @@ function VersionCheck(requiredVersion, installedVersion) {
|
||||
const [rMajor, rMinor, rPatch] = requiredVersion.split('.').map(Number);
|
||||
const [iMajor, iMinor, iPatch] = installedVersion.split('.').map(Number);
|
||||
|
||||
// 1. CRITICAL: Major versions must match exactly.
|
||||
if (iMajor !== rMajor) {
|
||||
console.log(`VersionCheck: Major version mismatch. Required: ${rMajor}, Installed: ${iMajor}`);
|
||||
return 'incompatible';
|
||||
}
|
||||
// Compare Major
|
||||
if (iMajor > rMajor) return 'compatible';
|
||||
if (iMajor < rMajor) return 'incompatible';
|
||||
|
||||
// 2. SOFT WARNING: The installed minor version is lower than what is required.
|
||||
if (iMinor < rMinor) {
|
||||
console.log(`VersionCheck: Minor version mismatch. Required: ${rMinor}, Installed: ${iMinor}`);
|
||||
return 'soft-warning';
|
||||
}
|
||||
// Major matches, compare Minor
|
||||
if (iMinor > rMinor) return 'compatible';
|
||||
if (iMinor < rMinor) return 'incompatible'; // or 'incompatible' depending on your policy
|
||||
|
||||
// 3. SILENT WARNING: Minors match, but the installed patch version is lagging.
|
||||
if (iMinor === rMinor && iPatch < rPatch) {
|
||||
console.log(`VersionCheck: Patch version mismatch. Required: ${rPatch}, Installed: ${iPatch}`);
|
||||
return 'compatible';
|
||||
}
|
||||
// Major and Minor match, compare Patch
|
||||
if (iPatch >= rPatch) return 'compatible';
|
||||
|
||||
// 4. PERFECT: Installed version meets or exceeds the required baseline.
|
||||
console.log(`VersionCheck: Installed version meets or exceeds the required baseline. Required: ${requiredVersion}, Installed: ${installedVersion}`);
|
||||
return 'compatible';
|
||||
return 'soft-warning'; // Installed patch is lower than required
|
||||
}
|
||||
|
||||
function SanitizeHTML(htmlString) {
|
||||
// 1. Parse the string into a temporary DOM document
|
||||
const parser = new DOMParser();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// If any other tag isn't in our allowed list, unwrap it
|
||||
if (!allowedTags.includes(el.tagName)) {
|
||||
const parent = el.parentNode;
|
||||
while (el.firstChild) {
|
||||
parent.insertBefore(el.firstChild, el);
|
||||
}
|
||||
parent.removeChild(el);
|
||||
return;
|
||||
}
|
||||
|
||||
// Strip out any attributes not explicitly allowed (like onerror, onload, etc.)
|
||||
Array.from(el.attributes).forEach(attr => {
|
||||
if (!allowedAttrs.includes(attr.name.toLowerCase())) {
|
||||
el.removeAttribute(attr.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return doc.body.innerHTML;
|
||||
}
|
||||
+41
-25
@@ -26,7 +26,11 @@ const line3 = document.getElementById('line3');
|
||||
// OPTIONS //
|
||||
/////////////
|
||||
|
||||
const language = urlParams.get("language") || "en";
|
||||
const timezone = urlParams.get("timezone") || "";
|
||||
|
||||
const font = urlParams.get("font") || "";
|
||||
const textAlignment = urlParams.get("textAlignment") || "center";
|
||||
|
||||
const enableLine1 = GetBooleanParam("enableLine1", true);
|
||||
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 line1FontOpacity = urlParams.get("line1FontOpacity") || "1";
|
||||
const line1TextTransform = urlParams.get("line1TextTransform") || "none";
|
||||
const line1TextAlignment = urlParams.get("line1TextAlignment") || "center";
|
||||
|
||||
const enableLine2 = GetBooleanParam("enableLine2", true);
|
||||
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 line2FontOpacity = urlParams.get("line2FontOpacity") || "0.7";
|
||||
const line2TextTransform = urlParams.get("line2TextTransform") || "none";
|
||||
const line2TextAlignment = urlParams.get("line2TextAlignment") || "center";
|
||||
|
||||
const enableLine3 = GetBooleanParam("enableLine3", false);
|
||||
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 line3FontOpacity = urlParams.get("line3FontOpacity") || "1";
|
||||
const line3TextTransform = urlParams.get("line3TextTransform") || "none";
|
||||
const line3TextAlignment = urlParams.get("line3TextAlignment") || "center";
|
||||
|
||||
|
||||
|
||||
@@ -63,7 +64,7 @@ const line3TextAlignment = urlParams.get("line3TextAlignment") || "center";
|
||||
|
||||
// Set the font for the entire page if specified
|
||||
if (font)
|
||||
document.body.style.fontFamily = `'${font}'`;
|
||||
document.body.style.fontFamily = `'${font}'`;
|
||||
|
||||
// Hide lines that are not enabled
|
||||
if (!enableLine1)
|
||||
@@ -75,45 +76,60 @@ if (!enableLine3)
|
||||
|
||||
|
||||
|
||||
////////////
|
||||
////////////////
|
||||
// CLOCKO //
|
||||
////////////
|
||||
////////////////
|
||||
|
||||
// Check if any active format string includes milliseconds (e.g., 'S', 'SS', 'SSS')
|
||||
const usesMilliseconds =
|
||||
(enableLine1 && line1Format.includes('S')) ||
|
||||
(enableLine2 && line2Format.includes('S')) ||
|
||||
(enableLine3 && line3Format.includes('S'));
|
||||
// Helper function to start the clock loop
|
||||
function StartClock() {
|
||||
// Check if any active format string includes milliseconds (e.g., 'S', 'SS', 'SSS')
|
||||
const usesMilliseconds =
|
||||
(enableLine1 && line1Format.includes('S')) ||
|
||||
(enableLine2 && line2Format.includes('S')) ||
|
||||
(enableLine3 && line3Format.includes('S'));
|
||||
|
||||
UpdateTime();
|
||||
UpdateTime();
|
||||
|
||||
if (usesMilliseconds) {
|
||||
// High-frequency updates for millisecond precision
|
||||
function updateFrame() {
|
||||
UpdateTime();
|
||||
if (usesMilliseconds) {
|
||||
function updateFrame() {
|
||||
UpdateTime();
|
||||
requestAnimationFrame(updateFrame);
|
||||
}
|
||||
requestAnimationFrame(updateFrame);
|
||||
} else {
|
||||
setInterval(UpdateTime, 1000);
|
||||
}
|
||||
requestAnimationFrame(updateFrame);
|
||||
} else {
|
||||
// Efficient 1-second interval for standard clocks
|
||||
setInterval(UpdateTime, 1000);
|
||||
}
|
||||
|
||||
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 (enableLine2) line2.textContent = now.format(line2Format);
|
||||
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 //
|
||||
/////////////
|
||||
|
||||
function ApplyStyling(el, fontSize, fontWeight, fontColor, fontOpacity, textTransform, textAlignment) {
|
||||
function ApplyStyling(el, fontSize, fontWeight, fontColor, fontOpacity, textTransform) {
|
||||
el.style.fontSize = fontSize + "px";
|
||||
el.style.fontWeight = fontWeight;
|
||||
el.style.color = fontColor;
|
||||
@@ -122,6 +138,6 @@ function ApplyStyling(el, fontSize, fontWeight, fontColor, fontOpacity, textTran
|
||||
el.style.textAlign = textAlignment;
|
||||
}
|
||||
|
||||
ApplyStyling(line1, line1FontSize, line1FontWeight, line1FontColor, line1FontOpacity, line1TextTransform, line1TextAlignment);
|
||||
ApplyStyling(line2, line2FontSize, line2FontWeight, line2FontColor, line2FontOpacity, line2TextTransform, line2TextAlignment);
|
||||
ApplyStyling(line3, line3FontSize, line3FontWeight, line3FontColor, line3FontOpacity, line3TextTransform, line3TextAlignment);
|
||||
ApplyStyling(line1, line1FontSize, line1FontWeight, line1FontColor, line1FontOpacity, line1TextTransform);
|
||||
ApplyStyling(line2, line2FontSize, line2FontWeight, line2FontColor, line2FontOpacity, line2TextTransform);
|
||||
ApplyStyling(line3, line3FontSize, line3FontWeight, line3FontColor, line3FontOpacity, line3TextTransform);
|
||||
@@ -1,5 +1,54 @@
|
||||
{
|
||||
"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",
|
||||
"label": "Font",
|
||||
@@ -8,6 +57,19 @@
|
||||
"defaultValue": "",
|
||||
"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",
|
||||
"label": "Enable Line 1",
|
||||
@@ -92,20 +154,6 @@
|
||||
"showIf": "enableLine1",
|
||||
"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",
|
||||
"label": "Enable Line 2",
|
||||
@@ -190,20 +238,6 @@
|
||||
"showIf": "enableLine2",
|
||||
"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",
|
||||
"label": "Enable Line 3",
|
||||
@@ -287,20 +321,6 @@
|
||||
"defaultValue": "none",
|
||||
"showIf": "enableLine3",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -75,6 +75,7 @@ const showPatreonMemberships = GetBooleanParam("showPatreonMemberships", true);
|
||||
const showKofiDonations = GetBooleanParam("showKofiDonations", true);
|
||||
const showTipeeeStreamDonations = GetBooleanParam("showTipeeeStreamDonations", true);
|
||||
const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", true);
|
||||
const skipFourthwallFreeOrders = GetBooleanParam("skipFourthwallFreeOrders", true);
|
||||
|
||||
const furryMode = GetBooleanParam("furryMode", false);
|
||||
|
||||
@@ -782,12 +783,12 @@ async function TwitchRewardRedemption(data) {
|
||||
if (!showTwitchChannelPointRedemptions)
|
||||
return;
|
||||
|
||||
let username = data.user_name;
|
||||
if (data.user_name.toLowerCase() != data.user_login.toLowerCase())
|
||||
username = `${data.user_name} (${data.user_login})`;
|
||||
let username = data.user_name ?? data.user.name;
|
||||
if (username.toLowerCase() != (data.user_login ?? data.user.login).toLowerCase())
|
||||
username = `${username} (${data.user_login ?? data.user.login})`;
|
||||
const rewardName = data.reward.title;
|
||||
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"/>`;
|
||||
|
||||
let message = `${username} redeemed ${rewardName} ${channelPointIcon} ${cost}`;
|
||||
@@ -1198,6 +1199,10 @@ function FourthwallOrderPlaced(data) {
|
||||
const item = data.variants[0].name;
|
||||
const itemsOrdered = data.variants.length;
|
||||
|
||||
// Skip free orders if the user has chosen to do so
|
||||
if (skipFourthwallFreeOrders && orderTotal == 0)
|
||||
return;
|
||||
|
||||
let message = "";
|
||||
|
||||
// If there user did not provide a username, just say "Someone"
|
||||
|
||||
@@ -379,6 +379,15 @@
|
||||
"defaultValue": true,
|
||||
"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",
|
||||
"label": "Furry Mode",
|
||||
|
||||
@@ -82,6 +82,7 @@ const showPatreonMemberships = GetBooleanParam("showPatreonMemberships", true);
|
||||
const showKofiDonations = GetBooleanParam("showKofiDonations", true);
|
||||
const showTipeeeStreamDonations = GetBooleanParam("showTipeeeStreamDonations", true);
|
||||
const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", true);
|
||||
const skipFourthwallFreeOrders = GetBooleanParam("skipFourthwallFreeOrders", true);
|
||||
|
||||
const furryMode = GetBooleanParam("furryMode", false);
|
||||
|
||||
@@ -90,7 +91,7 @@ const furryMode = GetBooleanParam("furryMode", false);
|
||||
////////////////////
|
||||
|
||||
const animationSpeed = GetIntParam("animationSpeed", 0.1);
|
||||
const randomYouTubeColors = GetBooleanParam("randomYouTubeColors", false);
|
||||
const randomYouTubeColors = GetBooleanParam("randomYouTubeColors", true);
|
||||
const youtubeColor = urlParams.get("youtubeColor") || "#f70000";
|
||||
const youtubeCustomSubIcon = urlParams.get("youtubeCustomSubIcon") || "";
|
||||
let twitchUsername = urlParams.get("twitchUsername") || "";
|
||||
@@ -1154,7 +1155,7 @@ async function TwitchRewardRedemption(data) {
|
||||
|
||||
if (showAvatar) {
|
||||
// Render avatars
|
||||
const username = data.user_login;
|
||||
const username = data.user_login ?? data.user.login;
|
||||
const avatarURL = await GetAvatar(username, 'twitch');
|
||||
const avatar = new Image();
|
||||
avatar.src = avatarURL;
|
||||
@@ -1163,12 +1164,12 @@ async function TwitchRewardRedemption(data) {
|
||||
}
|
||||
|
||||
// Set the text
|
||||
let username = data.user_name;
|
||||
if (data.user_name.toLowerCase() != data.user_login.toLowerCase())
|
||||
username = `${data.user_name} (${data.user_login})`;
|
||||
let username = data.user_name ?? data.user.name;
|
||||
if (username.toLowerCase() != (data.user_login ?? data.user.login).toLowerCase())
|
||||
username = `${username} (${data.user_login ?? data.user.login})`;
|
||||
const rewardName = data.reward.title;
|
||||
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"/>`;
|
||||
|
||||
titleDiv.innerHTML = `${username} redeemed ${rewardName} ${channelPointIcon} ${cost}`;
|
||||
@@ -2045,6 +2046,10 @@ function FourthwallOrderPlaced(data) {
|
||||
const itemImageUrl = data.variants[0].image;
|
||||
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;
|
||||
|
||||
let contents = "";
|
||||
|
||||
@@ -517,6 +517,15 @@
|
||||
"defaultValue": true,
|
||||
"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",
|
||||
"label": "Furry Mode",
|
||||
|
||||
@@ -112,6 +112,7 @@ const showTipeeeStreamDonations = GetBooleanParam("showTipeeeStreamDonations", f
|
||||
const tipeeestreamDonationAction = urlParams.get("tipeeestreamDonationAction") || "";
|
||||
const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", false);
|
||||
const fourthwallAlertAction = urlParams.get("fourthwallAlertAction") || "";
|
||||
const skipFourthwallFreeOrders = GetBooleanParam("skipFourthwallFreeOrders", true);
|
||||
|
||||
////////////////////
|
||||
// HIDDEN OPTIONS //
|
||||
@@ -687,14 +688,14 @@ async function TwitchRewardRedemption(data) {
|
||||
if (!showTwitchChannelPointRedemptions)
|
||||
return;
|
||||
|
||||
const username = data.user_name;
|
||||
const username = data.user_name ?? data.user.name;
|
||||
const rewardName = data.reward.title;
|
||||
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"/>`;
|
||||
|
||||
// Render avatars
|
||||
const avatarURL = await GetAvatar(data.user_login, 'twitch');
|
||||
const avatarURL = await GetAvatar(data.user_login ?? data.user.login, 'twitch');
|
||||
|
||||
UpdateAlertBox(
|
||||
'twitch',
|
||||
@@ -1124,6 +1125,10 @@ function FourthwallOrderPlaced(data) {
|
||||
const message = DecodeHTMLString(data.statmessageus);
|
||||
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 (user == undefined)
|
||||
user = "Someone";
|
||||
|
||||
@@ -628,6 +628,15 @@
|
||||
"showIf": "showFourthwallAlerts",
|
||||
"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",
|
||||
"label": "IP Address",
|
||||
|
||||
@@ -9,7 +9,7 @@ const urlParams = new URLSearchParams(queryString);
|
||||
// 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';
|
||||
let VersionChecked = false;
|
||||
|
||||
@@ -36,6 +36,8 @@ const useCustomColors = GetBooleanParam("useCustomColors", false);
|
||||
const color1 = urlParams.get('color1') || '#ffffff';
|
||||
const color2 = urlParams.get('color2') || '#1d1d1d';
|
||||
|
||||
const autoHide = GetBooleanParam("autoHide", false);
|
||||
const showWhilePaused = GetBooleanParam("showWhilePaused", false);
|
||||
const includedApplications = urlParams.get('includedApplications') || '';
|
||||
const excludedApplications = urlParams.get('excludedApplications') || '';
|
||||
const showAlbumArt = GetBooleanParam("showAlbumArt", true);
|
||||
@@ -43,7 +45,6 @@ const showProgressBar = GetBooleanParam("showProgressBar", true);
|
||||
const swapArtistTrack = GetBooleanParam("swapArtistTrack", false);
|
||||
const showPrimary = GetBooleanParam("showPrimary", true);
|
||||
const showSecondary = GetBooleanParam("showSecondary", true);
|
||||
const autoHide = GetBooleanParam("autoHide", false);
|
||||
const displayDuration = GetIntParam("displayDuration", 5);
|
||||
const showAnimation = urlParams.get('showAnimation') || 'slide-in-from-bottom';
|
||||
const hideAnimation = urlParams.get('hideAnimation') || 'slide-out-bottom';
|
||||
@@ -363,7 +364,7 @@ async function UpdatePlayerState(data) {
|
||||
for (const targetApp of includedList) {
|
||||
targetSession = validSessions.find(s => {
|
||||
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;
|
||||
});
|
||||
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
|
||||
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) {
|
||||
targetSession = playingSession;
|
||||
}
|
||||
@@ -411,7 +412,7 @@ async function UpdatePlayerState(data) {
|
||||
|
||||
// 1. Check if playback status has changed and update visibility accordingly
|
||||
if (playbackInfo.PlaybackStatus !== CurrentPlaybackStatus) {
|
||||
if (playbackInfo.PlaybackStatus === PlaybackStatus.PLAYING)
|
||||
if (playbackInfo.PlaybackStatus === PlaybackStatus.PLAYING || (showWhilePaused && playbackInfo.PlaybackStatus === PlaybackStatus.PAUSED))
|
||||
SetVisibility(true);
|
||||
else
|
||||
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
|
||||
// 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}`;
|
||||
if (newTrackKey !== CurrentSongKey) {
|
||||
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 currentPositionMs = isPlaying && timelineProps.EndTime > 0 ? timelineProps.Position + driftMs : timelineProps.Position;
|
||||
|
||||
SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette);
|
||||
SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, targetSession.playback_info.PlaybackStatus);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -150,6 +150,33 @@
|
||||
"showIf": "useCustomColors",
|
||||
"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",
|
||||
"label": "Included Apps",
|
||||
@@ -208,25 +235,6 @@
|
||||
"defaultValue": true,
|
||||
"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",
|
||||
"label": "Show Animation",
|
||||
|
||||
@@ -97,7 +97,7 @@ body {
|
||||
|
||||
@keyframes slide-in-from-left {
|
||||
0% {
|
||||
transform: translateX(-1em);
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -114,14 +114,14 @@ body {
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(-1em);
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-from-right {
|
||||
0% {
|
||||
transform: translateX(1em);
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ body {
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(1em);
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
<div id="album-art-container">
|
||||
<div id="album-art-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 id="text-info-container">
|
||||
|
||||
@@ -96,7 +96,12 @@ async function ChangeTrack(mediaProps, accentColorPalette) {
|
||||
}, 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
|
||||
// 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));
|
||||
progressBar.style.width = `${progressPercent}%`;
|
||||
if (!useCustomColors)
|
||||
progressBar.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
|
||||
document.body.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
|
||||
else
|
||||
progressBar.style.setProperty('--accent-color', color1);
|
||||
document.body.style.setProperty('--accent-color', color1);
|
||||
}
|
||||
@@ -21,6 +21,36 @@
|
||||
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-transition-layer {
|
||||
position: absolute;
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
<div id="album-art-container">
|
||||
<div id="album-art-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 id="track-label" class="text-label"> </div>
|
||||
|
||||
@@ -84,7 +84,7 @@ async function ChangeTrack(mediaProps, accentColorPalette) {
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) {
|
||||
// Update the label using your naming convention
|
||||
currentTimeLabel.innerText =
|
||||
ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs);
|
||||
@@ -92,6 +92,12 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
durationLabel.innerText =
|
||||
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
|
||||
// Ensure we don't divide by zero or exceed 100%
|
||||
const durationMs = timelineProps.EndTime;
|
||||
|
||||
@@ -23,6 +23,36 @@
|
||||
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-transition-layer {
|
||||
position: absolute;
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
<div id="album-art-container">
|
||||
<div id="album-art-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 id="song-info-container">
|
||||
|
||||
@@ -84,7 +84,7 @@ async function ChangeTrack(mediaProps, accentColorPalette) {
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) {
|
||||
// Update the label using your naming convention
|
||||
currentTimeLabel.innerText =
|
||||
ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs);
|
||||
@@ -92,6 +92,12 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
durationLabel.innerText =
|
||||
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
|
||||
// Ensure we don't divide by zero or exceed 100%
|
||||
const durationMs = timelineProps.EndTime;
|
||||
|
||||
@@ -24,6 +24,37 @@
|
||||
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-transition-layer {
|
||||
position: absolute;
|
||||
|
||||
@@ -94,7 +94,7 @@ async function ChangeTrack(mediaProps, accentColorPalette) {
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) {
|
||||
const durationMs = timelineProps.EndTime;
|
||||
|
||||
let progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0;
|
||||
|
||||
@@ -97,7 +97,7 @@ async function ChangeTrack(mediaProps, accentColorPalette) {
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) {
|
||||
// Set progressbar
|
||||
// Ensure we don't divide by zero or exceed 100%
|
||||
const durationMs = timelineProps.EndTime;
|
||||
@@ -122,6 +122,12 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
// const transitionWidth = '1em';
|
||||
// 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}%)`;
|
||||
|
||||
// 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
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
grid-template-areas: "stack";
|
||||
}
|
||||
|
||||
#song-info-container.is-paused {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* The content sits on top */
|
||||
#progress-bar {
|
||||
position: relative;
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
<div id="album-art-container">
|
||||
<div id="album-art-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 id="song-info-container">
|
||||
|
||||
@@ -78,7 +78,7 @@ async function ChangeTrack(mediaProps) {
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) {
|
||||
// Update the label using your naming convention
|
||||
currentTimeLabel.innerText =
|
||||
ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs);
|
||||
@@ -86,6 +86,12 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
durationLabel.innerText =
|
||||
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
|
||||
// Ensure we don't divide by zero or exceed 100%
|
||||
const durationMs = timelineProps.EndTime;
|
||||
@@ -95,12 +101,12 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
|
||||
if (!useCustomColors)
|
||||
{
|
||||
progressBarFill.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
|
||||
document.body.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
|
||||
document.body.style.color = accentColorPalette.LightVibrant;
|
||||
}
|
||||
else
|
||||
{
|
||||
progressBarFill.style.setProperty('--accent-color', color1);
|
||||
document.body.style.setProperty('--accent-color', color1);
|
||||
document.body.style.color = color1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,37 @@
|
||||
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-transition-layer {
|
||||
position: absolute;
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
<div id="album-art-container">
|
||||
<div id="album-art-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 id="song-info-wrapper">
|
||||
|
||||
@@ -147,7 +147,7 @@ async function ChangeTrack(mediaProps, accentColorPalette) {
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) {
|
||||
// Update the label using your naming convention
|
||||
currentTimeLabel.innerText =
|
||||
ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs);
|
||||
@@ -155,6 +155,12 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
durationLabel.innerText =
|
||||
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
|
||||
// Ensure we don't divide by zero or exceed 100%
|
||||
const durationMs = timelineProps.EndTime;
|
||||
@@ -165,9 +171,11 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
if (!useCustomColors) {
|
||||
switch (themeVariant) {
|
||||
case "matte":
|
||||
progressBarFill.style.setProperty('--accent-color', accentColorPalette.DarkVibrant);
|
||||
document.body.style.setProperty('--accent-color', accentColorPalette.DarkVibrant);
|
||||
break;
|
||||
case "matte-dark":
|
||||
document.body.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
|
||||
break;
|
||||
default:
|
||||
progressBarFill.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
|
||||
break;
|
||||
@@ -177,11 +185,11 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
{
|
||||
switch (themeVariant) {
|
||||
case "matte":
|
||||
progressBarFill.style.setProperty('--accent-color', color2);
|
||||
document.body.style.setProperty('--accent-color', color2);
|
||||
break;
|
||||
// case "matte-dark":
|
||||
default:
|
||||
progressBarFill.style.setProperty('--accent-color', color1);
|
||||
document.body.style.setProperty('--accent-color', color1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,36 @@
|
||||
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-transition-layer {
|
||||
position: absolute;
|
||||
|
||||
@@ -8,6 +8,7 @@ const albumArtContainer = document.getElementById('album-art-container');
|
||||
const albumArtLayer = document.getElementById('album-art-layer');
|
||||
const albumArtTransition = document.getElementById('album-art-transition-layer');
|
||||
const progressCircle = document.getElementById('progress-pie-circle');
|
||||
const vinylContainer = document.getElementById('vinyl-container');
|
||||
|
||||
///////////////
|
||||
// CONSTANTS //
|
||||
@@ -60,7 +61,7 @@ async function ChangeTrack(mediaProps) {
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) {
|
||||
// Set progressbar
|
||||
// Ensure we don't divide by zero or exceed 100%
|
||||
const durationMs = timelineProps.EndTime;
|
||||
@@ -75,4 +76,52 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
|
||||
// 0% progress = 157.1 offset. 100% progress = 0 offset.
|
||||
const offset = circumference - (progressPercent / 100) * circumference;
|
||||
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);
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
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 {
|
||||
@@ -91,7 +91,64 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ async function CustomEvent(data) {
|
||||
subtitleEl.innerText = `${data.user}`;
|
||||
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.innerHTML = data.message;
|
||||
messageEl.innerHTML = SanitizeHTML(data.message);
|
||||
|
||||
// Render emotes
|
||||
for (i in data.emotes) {
|
||||
@@ -116,7 +116,7 @@ async function CustomEvent(data) {
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.innerHTML = `<b>${data.cumulative} months${data.monthStreak > 1 ? ' (' + data.monthStreak + ' in a row!)' : ''}</b>`;
|
||||
if (data.messageStripped)
|
||||
messageEl.innerHTML += `<br><br><i>${data.messageStripped}</i>`;
|
||||
messageEl.innerHTML += `<br><br><i>${SanitizeHTML(data.messageStripped)}</i>`;
|
||||
|
||||
contentEl.appendChild(messageEl);
|
||||
|
||||
@@ -196,7 +196,7 @@ async function CustomEvent(data) {
|
||||
subtitleEl.innerText = `${data.user}`;
|
||||
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.innerHTML = data.rawInput;
|
||||
messageEl.innerHTML = SanitizeHTML(data.rawInput);
|
||||
|
||||
// // Render emotes
|
||||
// for (i in data.emotes) {
|
||||
@@ -284,7 +284,7 @@ async function CustomEvent(data) {
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.innerHTML = `<b>${data.user}</b><br>sent a Super Chat!`;
|
||||
if (data.message)
|
||||
messageEl.innerHTML += `<br><br><i>${data.message}</i>`;
|
||||
messageEl.innerHTML += `<br><br><i>${SanitizeHTML(data.message)}</i>`;
|
||||
|
||||
contentEl.appendChild(messageEl);
|
||||
|
||||
@@ -424,7 +424,7 @@ async function CustomEvent(data) {
|
||||
|
||||
if (data.tipMessage) {
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.innerHTML = `<i>${data.tipMessage}</i>`;
|
||||
messageEl.innerHTML = `<i>${SanitizeHTML(data.tipMessage)}</i>`;
|
||||
|
||||
contentEl.appendChild(messageEl);
|
||||
}
|
||||
@@ -448,7 +448,7 @@ async function CustomEvent(data) {
|
||||
|
||||
if (data.donationMessage) {
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.innerHTML = `<i>${data.donationMessage}</i>`;
|
||||
messageEl.innerHTML = `<i>${SanitizeHTML(data.donationMessage)}</i>`;
|
||||
|
||||
contentEl.appendChild(messageEl);
|
||||
}
|
||||
@@ -475,7 +475,7 @@ async function CustomEvent(data) {
|
||||
|
||||
if (data["fw.message"]) {
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.innerHTML = `<i>${data["fw.message"]}</i>`;
|
||||
messageEl.innerHTML = `<i>${SanitizeHTML(data["fw.message"])}</i>`;
|
||||
|
||||
contentEl.appendChild(messageEl);
|
||||
}
|
||||
@@ -537,7 +537,7 @@ async function CustomEvent(data) {
|
||||
if (customMessage) {
|
||||
const txt = document.createElement("textarea");
|
||||
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
|
||||
|
||||
@@ -146,7 +146,7 @@ client.on('Twitch.UpcomingAd', (response) => {
|
||||
/////////////////////
|
||||
|
||||
function TwitchAdRun(data) {
|
||||
const duration = data.length_seconds;
|
||||
const duration = data.lengthSeconds ?? data.length_seconds;
|
||||
|
||||
// Unset the upcoming ad countdown warning
|
||||
upcomingAdWarningStartDelay = null;
|
||||
|
||||
Reference in New Issue
Block a user