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();
|
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
@@ -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
@@ -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);
|
||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 = "";
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"> </div>
|
<div id="track-label" class="text-label"> </div>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user