diff --git a/.common/core/settings-core/script.js b/.common/core/settings-core/script.js
index f0f0e8c..ab4af18 100644
--- a/.common/core/settings-core/script.js
+++ b/.common/core/settings-core/script.js
@@ -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);
+ }
+}
+
/////////////////////////
@@ -599,4 +638,7 @@ LoadSettingsFromStorage();
LoadJSON(settingsJson);
// Populate local fonts for auto-suggest
-PopulateFontDatalist();
\ No newline at end of file
+PopulateFontDatalist();
+
+// Populate timezones for auto-suggest
+PopulateTimezoneDatalist();
diff --git a/.common/utils/helpers.js b/.common/utils/helpers.js
index d7c4b99..2014a42 100644
--- a/.common/utils/helpers.js
+++ b/.common/utils/helpers.js
@@ -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 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
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;
}
\ No newline at end of file
diff --git a/clocko/script.js b/clocko/script.js
index 81bbd89..9f23f87 100644
--- a/clocko/script.js
+++ b/clocko/script.js
@@ -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);
\ No newline at end of file
+ApplyStyling(line1, line1FontSize, line1FontWeight, line1FontColor, line1FontOpacity, line1TextTransform);
+ApplyStyling(line2, line2FontSize, line2FontWeight, line2FontColor, line2FontOpacity, line2TextTransform);
+ApplyStyling(line3, line3FontSize, line3FontWeight, line3FontColor, line3FontOpacity, line3TextTransform);
\ No newline at end of file
diff --git a/clocko/settings/settings.json b/clocko/settings/settings.json
index de49bb5..7b81529 100644
--- a/clocko/settings/settings.json
+++ b/clocko/settings/settings.json
@@ -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"
}
]
}
\ No newline at end of file
diff --git a/horizontal-chat/script.js b/horizontal-chat/script.js
index d2170ef..23ef007 100644
--- a/horizontal-chat/script.js
+++ b/horizontal-chat/script.js
@@ -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 = `
`;
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"
diff --git a/horizontal-chat/settings/settings.json b/horizontal-chat/settings/settings.json
index 7ecd4b6..a99b145 100644
--- a/horizontal-chat/settings/settings.json
+++ b/horizontal-chat/settings/settings.json
@@ -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",
diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js
index 374e95a..b731db9 100644
--- a/multichat-overlay/script.js
+++ b/multichat-overlay/script.js
@@ -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 = `
`;
titleDiv.innerHTML = `${username} redeemed ${rewardName} ${channelPointIcon} ${cost}`;
@@ -2045,6 +2046,10 @@ function FourthwallOrderPlaced(data) {
const itemImageUrl = data.variants[0].image;
const fourthwallProductImage = ``;
+ // Skip free orders if the user has chosen to do so
+ if (skipFourthwallFreeOrders && orderTotal == 0)
+ return;
+
avatarDiv.innerHTML = fourthwallProductImage;
let contents = "";
diff --git a/multichat-overlay/settings/settings.json b/multichat-overlay/settings/settings.json
index 3c73b97..fee2d12 100644
--- a/multichat-overlay/settings/settings.json
+++ b/multichat-overlay/settings/settings.json
@@ -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",
diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js
index 6721483..7638a4a 100644
--- a/multistream-alerts/script.js
+++ b/multistream-alerts/script.js
@@ -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 = `
`;
// 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";
diff --git a/multistream-alerts/settings/settings.json b/multistream-alerts/settings/settings.json
index 49bd7f9..ec19143 100644
--- a/multistream-alerts/settings/settings.json
+++ b/multistream-alerts/settings/settings.json
@@ -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",
diff --git a/now-playing/script.js b/now-playing/script.js
index f910ced..697bbe8 100644
--- a/now-playing/script.js
+++ b/now-playing/script.js
@@ -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 {
diff --git a/now-playing/settings/settings.json b/now-playing/settings/settings.json
index ee532ca..39a677f 100644
--- a/now-playing/settings/settings.json
+++ b/now-playing/settings/settings.json
@@ -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",
diff --git a/now-playing/style.css b/now-playing/style.css
index de16278..f33f7eb 100644
--- a/now-playing/style.css
+++ b/now-playing/style.css
@@ -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;
}
}
diff --git a/now-playing/themes/album-art/index.html b/now-playing/themes/album-art/index.html
index bfac360..a75a2ec 100644
--- a/now-playing/themes/album-art/index.html
+++ b/now-playing/themes/album-art/index.html
@@ -3,6 +3,11 @@