diff --git a/.common/core/settings-core/script.js b/.common/core/settings-core/script.js
index 3e76669..6868574 100644
--- a/.common/core/settings-core/script.js
+++ b/.common/core/settings-core/script.js
@@ -4,6 +4,7 @@ const urlParams = new URLSearchParams(queryString);
const settingsJson = urlParams.get("settingsJson") || "";
const widgetURL = urlParams.get("widgetURL") || "";
const showUnmuteIndicator = GetBooleanParam("showUnmuteIndicator", false);
+const useStreamerBot = GetBooleanParam("usesStreamerBot", false);
// Page elements
const widgetUrlInputWrapper = document.getElementById('widgetUrlInputWrapper');
@@ -101,6 +102,7 @@ function LoadJSON(settingsJson) {
inputElement.id = setting.id; //Added setting ID
inputElement.value = settingsMap.has(setting.id) ? settingsMap.get(setting.id) : setting.defaultValue;
inputElement.autocomplete = 'new-password';
+ inputElement.placeholder = setting.placeholder ? setting.placeholder : '';
break;
case 'password':
inputElement = document.createElement('input');
@@ -168,6 +170,24 @@ function LoadJSON(settingsJson) {
inputElement.setAttribute('list', 'streamer-bot-actions');
inputElement.autocomplete = 'off';
break;
+ case 'font':
+ inputElement = document.createElement('input');
+ inputElement.type = 'text';
+ inputElement.placeholder = 'Type to search...';
+ inputElement.id = setting.id; //Added setting ID
+ inputElement.value = settingsMap.has(setting.id) ? settingsMap.get(setting.id) : setting.defaultValue;
+ inputElement.setAttribute('list', 'fonts');
+ inputElement.autocomplete = 'off';
+
+ // Trigger permission prompt and load fonts on first click/focus
+ inputElement.addEventListener('focus', async function loadOnce() {
+ // Remove listener so it only triggers once per session
+ inputElement.removeEventListener('focus', loadOnce);
+ inputElement.placeholder = 'Type to search...';
+
+ await PopulateFontDatalist();
+ }, { once: true });
+ break;
case 'button':
inputElement = document.createElement('button');
inputElement.id = setting.id; //Added setting ID
@@ -211,6 +231,7 @@ function LoadJSON(settingsJson) {
SaveSettingsToStorage();
RefreshWidgetPreview();
+ InterpolatePlaceholders();
});
settingItemContent.appendChild(inputElement);
@@ -257,6 +278,7 @@ function LoadJSON(settingsJson) {
UpdateSettingItemVisibility();
RefreshWidgetPreview();
SaveSettingsToStorage();
+ InterpolatePlaceholders();
})
.catch(error => console.error('Error loading settings:', error));
}
@@ -315,6 +337,9 @@ function RefreshWidgetPreview() {
}
function UpdateStreamerBotConnection() {
+ if (!useStreamerBot)
+ return;
+
let addressElement = document.getElementById('address');
let portElement = document.getElementById('port');
@@ -370,6 +395,38 @@ async function GetSBActions() {
document.body.appendChild(datalistElement);
}
+async function PopulateFontDatalist() {
+ if (!('queryLocalFonts' in window)) {
+ console.warn("Local Font Access API is not supported in this browser. Font auto-suggestions will be disabled.");
+ return;
+ }
+
+ try {
+ // Request permission and fetch local fonts
+ const availableFonts = await window.queryLocalFonts();
+
+ // Extract unique font family names and sort them alphabetically
+ const fontFamilies = [...new Set(availableFonts.map(font => font.family))].sort();
+
+ // Create the datalist element
+ const datalistElement = document.createElement('datalist');
+ datalistElement.id = 'fonts';
+
+ // Append each font family as an option
+ fontFamilies.forEach(family => {
+ const option = document.createElement('option');
+ option.value = family;
+ datalistElement.appendChild(option);
+ });
+
+ document.body.appendChild(datalistElement);
+ console.debug(`Loaded ${fontFamilies.length} local fonts into auto-suggest.`);
+
+ } catch (err) {
+ console.error("Permission denied or error fetching local fonts:", err);
+ }
+}
+
/////////////////////////
@@ -476,15 +533,56 @@ window.addEventListener('message', (event) => {
});
function GetSettingDepth(setting, allSettings) {
- let depth = 0;
- let current = setting;
+ let depth = 0;
+ let current = setting;
- while (current.showIf) {
- depth++;
- current = allSettings.find(s => s.id === current.showIf) || {};
- }
+ while (current.showIf) {
+ depth++;
+ current = allSettings.find(s => s.id === current.showIf) || {};
+ }
- return depth;
+ return depth;
+}
+
+function InterpolatePlaceholders() {
+ // Regex to match anything inside curly braces, e.g., {smtcBridgeAddress}
+ const tokenRegex = /\{([^}]+)\}/g;
+
+ // Iterate over all settings from the original JSON in memory
+ settingsData.settings.forEach(setting => {
+
+ // Only process settings that actually have a placeholder in their description
+ if (setting.description && setting.description.includes('{')) {
+
+ // Start with the original untouched description from the JSON
+ let dynamicHTML = setting.description;
+ let match;
+
+ // Reset regex state (required when reusing a global regex in a loop)
+ tokenRegex.lastIndex = 0;
+
+ // Find all {tokens} in the string
+ while ((match = tokenRegex.exec(setting.description)) !== null) {
+ const targetId = match[1]; // The exact text inside the braces
+ const targetInput = document.getElementById(targetId);
+
+ if (targetInput) {
+ // Get current value, or fallback to the JSON default if the box is empty
+ const fallback = settingsData.settings.find(s => s.id === targetId)?.defaultValue || '';
+ const currentValue = targetInput.value || fallback;
+
+ // Replace the token with the actual value in our temporary string
+ dynamicHTML = dynamicHTML.replace(match[0], currentValue);
+ }
+ }
+
+ // Find the specific
tag for this setting in the DOM and overwrite its HTML
+ const descriptionParagraph = document.querySelector(`#item-${setting.id} p`);
+ if (descriptionParagraph) {
+ descriptionParagraph.innerHTML = dynamicHTML;
+ }
+ }
+ });
}
@@ -494,4 +592,7 @@ function GetSettingDepth(setting, allSettings) {
LoadSettingsFromStorage();
// Load default settings
-LoadJSON(settingsJson);
\ No newline at end of file
+LoadJSON(settingsJson);
+
+// Populate local fonts for auto-suggest
+PopulateFontDatalist();
\ No newline at end of file
diff --git a/.common/enums/smtc-enums.js b/.common/enums/smtc-enums.js
new file mode 100644
index 0000000..365d7e0
--- /dev/null
+++ b/.common/enums/smtc-enums.js
@@ -0,0 +1,28 @@
+// ==========================================
+// WINDOWS SMTC API CONSTANTS (IMMUTABLE)
+// ==========================================
+
+// Playback Status (session.playback_info.PlaybackStatus)
+const PlaybackStatus = Object.freeze({
+ CLOSED: 0, // Engine uninitialized or empty
+ OPENED: 1, // Pipeline loaded but idling
+ CHANGING: 2, // Buffering, track skipping, or seeking
+ STOPPED: 3, // Track queued but fully stopped (at 0:00)
+ PLAYING: 4, // Audio actively streaming (Run dead reckoning)
+ PAUSED: 5 // Audio frozen (Halt dead reckoning)
+});
+
+// Playback Type (session.playback_info.PlaybackType)
+const PlaybackType = Object.freeze({
+ UNKNOWN: 0, // Generic audio wrapper
+ MUSIC: 1, // Pure audio pipeline (Spotify, iTunes, etc.)
+ VIDEO: 2, // Visual media feed (Chrome/Firefox YouTube/Twitch tabs)
+ IMAGE: 3 // Static slideshow presentation hook
+});
+
+// Auto Repeat Mode (session.playback_info.AutoRepeatMode)
+const AutoRepeatMode = Object.freeze({
+ NONE: 0, // Plays queue through and terminates
+ TRACK: 1, // Single active song looping indefinitely
+ LIST: 2 // Parent playlist/album looping indefinitely
+});
\ No newline at end of file
diff --git a/.common/resources/smtc-bridge-icon.png b/.common/resources/smtc-bridge-icon.png
new file mode 100644
index 0000000..a17927d
Binary files /dev/null and b/.common/resources/smtc-bridge-icon.png differ
diff --git a/.common/utils/helpers.js b/.common/utils/helpers.js
index a54e3ae..d7c4b99 100644
--- a/.common/utils/helpers.js
+++ b/.common/utils/helpers.js
@@ -411,4 +411,302 @@ async function LoadHTMLTemplate(name) {
if (!document.getElementById(template.id)) {
document.body.appendChild(template.cloneNode(true));
}
+}
+
+function ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(time) {
+ if (isNaN(time) || time <= 0) return "0:00";
+
+ const totalSeconds = Math.floor(time / 1000);
+ const hours = Math.floor(totalSeconds / 3600);
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
+ const seconds = totalSeconds % 60;
+
+ // Format seconds with a leading zero
+ const paddedSeconds = ('0' + seconds).slice(-2);
+
+ if (hours > 0) {
+ // Format minutes with a leading zero if hours are present
+ const paddedMinutes = ('0' + minutes).slice(-2);
+ return `${hours}:${paddedMinutes}:${paddedSeconds}`;
+ }
+
+ return `${minutes}:${paddedSeconds}`;
+}
+
+async function GetAccentPalette(imageUrl) {
+ // 1. Dynamic Loader: Load Vibrant.js
+ if (typeof Vibrant === 'undefined') {
+ await new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+ script.src = "https://cdnjs.cloudflare.com/ajax/libs/node-vibrant/3.1.6/vibrant.min.js";
+ script.onload = resolve;
+ script.onerror = reject;
+ document.head.appendChild(script);
+ });
+ }
+
+ return new Promise((resolve) => {
+ // Vibrant can take the URL directly!
+ Vibrant.from(imageUrl).getPalette((err, palette) => {
+ if (err) {
+ console.warn("Vibrant failed, using fallback.");
+ return resolve({
+ Vibrant: "#ffffff",
+ Muted: "#cccccc",
+ DarkVibrant: "#000000"
+ });
+ }
+
+ // Extract Hex from each swatch
+ const hexPalette = {};
+ for (let role in palette) {
+ if (palette[role]) {
+ hexPalette[role] = palette[role].getHex();
+ }
+ }
+ resolve(hexPalette);
+ });
+ });
+}
+
+// Generic popup
+function SplashscreenPopup(iconSrc, title, subtitle, attribute, background, button) {
+ // 1. Check if a popup is already on screen
+ const existingOverlay = document.getElementById('global-common-overlay');
+
+ // If it exists AND isn't already in the middle of fading out, DO NOTHING
+ if (existingOverlay && !existingOverlay.classList.contains('common-closing')) {
+ return null;
+ }
+
+ // If there IS a popup but it IS fading out, kill it instantly to make room for the new one
+ if (existingOverlay) {
+ existingOverlay.remove();
+ }
+
+ // 2. Inject global styles if they don't already exist in the head
+ if (!document.getElementById('global-common-popup-styles')) {
+ const styleSheet = document.createElement('style');
+ styleSheet.id = 'global-common-popup-styles';
+ styleSheet.innerText = `
+ #global-common-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: transparent;
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 999999;
+ animation: commonFadeIn 2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
+ }
+ .common-popup-card {
+ width: 90%;
+ max-width: 500px;
+ border-radius: 24px;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ overflow: hidden;
+ box-shadow: 0 30px 60px rgba(0,0,0,0.4),
+ 0 0 0 1px rgba(255, 255, 255, 0.08);
+ animation: commonScaleIn 2.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
+ gap: 32px;
+ padding: 32px;
+ }
+ .common-popup-icon-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ }
+ .common-popup-img {
+ height: 48px;
+ width: auto;
+ object-fit: contain;
+ display: block;
+ }
+ .common-popup-text-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ text-align: left;
+ gap: 8px;
+ }
+ .common-popup-title {
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-size: 22px;
+ font-weight: 700;
+ color: #ffffff;
+ letter-spacing: -0.02em;
+ line-height: 1.2;
+ }
+ .common-popup-title:last-child {
+ margin-bottom: 0;
+ }
+ .common-popup-subtitle {
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-size: 14px;
+ font-weight: 400;
+ color: rgba(255, 255, 255, 0.65);
+ margin: 0;
+ line-height: 1.5;
+ }
+ .common-popup-attribute {
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-size: 13px;
+ color: rgba(255, 255, 255, 0.4);
+ word-break: break-all;
+ }
+ /* 💎 GLASSMORPHIC BUTTON STYLES */
+ .common-popup-button {
+ margin-top: 6px;
+ padding: 10px 20px;
+ border-radius: 12px;
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ background: rgba(255, 255, 255, 0.1);
+ color: #ffffff;
+ font-family: system-ui, sans-serif;
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s ease, border-color 0.2s ease, transform 0.1s ease;
+ text-align: center;
+ width: fit-content;
+ }
+ .common-popup-button:hover {
+ background: rgba(255, 255, 255, 0.2);
+ border-color: rgba(255, 255, 255, 0.3);
+ }
+ .common-popup-button:active {
+ transform: scale(0.97);
+ }
+ @keyframes commonFadeIn {
+ from { opacity: 0; } to { opacity: 1; }
+ }
+ @keyframes commonScaleIn {
+ from { transform: scale(0.95) translateY(10px); opacity: 0; }
+ to { transform: scale(1) translateY(0); opacity: 1; }
+ }
+ #global-common-overlay.common-closing {
+ animation: commonFadeOut 2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
+ }
+ .common-closing .common-popup-card {
+ animation: commonScaleOut 2.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
+ }
+ @keyframes commonFadeOut {
+ from { opacity: 1; } to { opacity: 0; }
+ }
+ @keyframes commonScaleOut {
+ from { transform: scale(1) translateY(0); opacity: 1; }
+ to { transform: scale(0.95) translateY(10px); opacity: 0; }
+ }
+ `;
+ document.head.appendChild(styleSheet);
+ }
+
+ // 3. Create elements
+ const overlay = document.createElement('div');
+ overlay.id = 'global-common-overlay';
+
+ const card = document.createElement('div');
+ card.className = 'common-popup-card';
+ card.style.background = background || 'linear-gradient(135deg, #1a1a1e 0%, #111113 100%)';
+
+ // 4. Construct Inner Content (Strict Conditional Rendering)
+ const iconMarkup = iconSrc
+ ? `
`
+ : '';
+
+ const titleMarkup = title
+ ? ``
+ : '';
+
+ const subtitleMarkup = subtitle
+ ? ``
+ : '';
+
+ const attributeMarkup = attribute
+ ? ``
+ : '';
+
+ // Render button conditional markup
+ const buttonMarkup = (button && button.text)
+ ? ``
+ : '';
+
+ card.innerHTML = `
+ ${iconMarkup}
+
+ `;
+
+ // 5. Setup Action Listeners if Button Exists
+ if (button && button.text && button.action) {
+ const actionBtn = card.querySelector('.common-popup-button');
+ if (actionBtn) {
+ actionBtn.addEventListener('click', () => {
+ if (typeof button.action === 'function') {
+ button.action();
+ } else if (typeof button.action === 'string') {
+ window.open(button.action, '_blank');
+ }
+ });
+ }
+ }
+
+ // 6. Append to page DOM
+ overlay.appendChild(card);
+ document.body.appendChild(overlay);
+
+ return {
+ element: overlay,
+ close: () => {
+ overlay.classList.add('common-closing');
+ overlay.addEventListener('animationend', (event) => {
+ if (event.target === overlay) {
+ overlay.remove();
+ }
+ });
+ }
+ };
+}
+
+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';
+ }
+
+ // 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';
+ }
+
+ // 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';
+ }
+
+ // 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';
}
\ No newline at end of file
diff --git a/clocko/index.html b/clocko/index.html
new file mode 100644
index 0000000..8078048
--- /dev/null
+++ b/clocko/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+'
+'
+
\ No newline at end of file
diff --git a/clocko/script.js b/clocko/script.js
new file mode 100644
index 0000000..81bbd89
--- /dev/null
+++ b/clocko/script.js
@@ -0,0 +1,127 @@
+/////////////
+// IMPORTS //
+/////////////
+
+dayjs.extend(window.dayjs_plugin_utc);
+dayjs.extend(window.dayjs_plugin_timezone);
+dayjs.extend(window.dayjs_plugin_advancedFormat);
+
+////////////////////
+// URL PARAMETERS //
+////////////////////
+
+const queryString = window.location.search;
+const urlParams = new URLSearchParams(queryString);
+
+///////////////////
+// PAGE ELEMENTS //
+///////////////////
+
+const mainContainer = document.getElementById('main-container');
+const line1 = document.getElementById('line1');
+const line2 = document.getElementById('line2');
+const line3 = document.getElementById('line3');
+
+/////////////
+// OPTIONS //
+/////////////
+
+const font = urlParams.get("font") || "";
+
+const enableLine1 = GetBooleanParam("enableLine1", true);
+const line1Format = urlParams.get("line1Format") || "hh:mm:ss A";
+const line1FontSize = GetIntParam("line1FontSize", 50);
+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";
+const line2FontSize = GetIntParam("line2FontSize", 40);
+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";
+const line3FontSize = GetIntParam("line3FontSize", 30);
+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";
+
+
+
+////////////////
+// PAGE SETUP //
+////////////////
+
+// Set the font for the entire page if specified
+if (font)
+ document.body.style.fontFamily = `'${font}'`;
+
+// Hide lines that are not enabled
+if (!enableLine1)
+ line1.style.display = "none";
+if (!enableLine2)
+ line2.style.display = "none";
+if (!enableLine3)
+ line3.style.display = "none";
+
+
+
+////////////
+// 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'));
+
+UpdateTime();
+
+if (usesMilliseconds) {
+ // High-frequency updates for millisecond precision
+ function updateFrame() {
+ UpdateTime();
+ requestAnimationFrame(updateFrame);
+ }
+ requestAnimationFrame(updateFrame);
+} else {
+ // Efficient 1-second interval for standard clocks
+ setInterval(UpdateTime, 1000);
+}
+
+function UpdateTime() {
+ const now = dayjs().tz(dayjs.tz.guess());
+
+ if (enableLine1) line1.textContent = now.format(line1Format);
+ if (enableLine2) line2.textContent = now.format(line2Format);
+ if (enableLine3) line3.textContent = now.format(line3Format);
+}
+
+
+
+/////////////
+// STYLING //
+/////////////
+
+function ApplyStyling(el, fontSize, fontWeight, fontColor, fontOpacity, textTransform, textAlignment) {
+ el.style.fontSize = fontSize + "px";
+ el.style.fontWeight = fontWeight;
+ el.style.color = fontColor;
+ el.style.opacity = fontOpacity;
+ el.style.textTransform = textTransform;
+ 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
diff --git a/clocko/settings/index.html b/clocko/settings/index.html
new file mode 100644
index 0000000..2fd0841
--- /dev/null
+++ b/clocko/settings/index.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ nutty
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/clocko/settings/script.js b/clocko/settings/script.js
new file mode 100644
index 0000000..f067f52
--- /dev/null
+++ b/clocko/settings/script.js
@@ -0,0 +1,26 @@
+const widgetContainer = document.getElementById('widgetContainer');
+
+const settingsPageURL = '/.common/core/settings-core';
+
+const currentURL = window.location.href;
+
+let settingsJSON;
+let baseURL = currentURL;
+
+if (baseURL.endsWith("index.html"))
+ baseURL = baseURL.replace("index.html", "");
+
+settingsJSON = "?settingsJson=" + baseURL + "settings.json";
+
+const lastSlashIndex = baseURL.lastIndexOf("/");
+let widgetURL = "&widgetURL=" + baseURL.replace("/settings", "");
+
+const usesStreamerBot = "&usesStreamerBot=false";
+
+console.debug("Window Ref: " + window.location.href);
+console.debug("Base URL: " + baseURL);
+console.debug("Settings JSON: " + settingsJSON);
+console.debug("Widget URL: " + widgetURL);
+console.debug("Uses Streamer Bot: " + usesStreamerBot);
+
+widgetContainer.src = settingsPageURL + settingsJSON + widgetURL + usesStreamerBot;
\ No newline at end of file
diff --git a/clocko/settings/settings.json b/clocko/settings/settings.json
new file mode 100644
index 0000000..bf27ef7
--- /dev/null
+++ b/clocko/settings/settings.json
@@ -0,0 +1,306 @@
+{
+ "settings": [
+ {
+ "id": "font",
+ "label": "Font",
+ "description": "Check installed fonts",
+ "type": "font",
+ "defaultValue": "",
+ "group": "Appearance"
+ },
+ {
+ "id": "enableLine1",
+ "label": "Enable Line 1",
+ "description": "Show the first line of the clock",
+ "type": "checkbox",
+ "defaultValue": true,
+ "group": "Appearance"
+ },
+ {
+ "id": "line1Format",
+ "label": "Format",
+ "description": "Click here for formatting options",
+ "type": "text",
+ "defaultValue": "hh:mm:ss A",
+ "showIf": "enableLine1",
+ "group": "Appearance"
+ },
+ {
+ "id": "line1FontSize",
+ "label": "Font Size",
+ "description": "",
+ "type": "number",
+ "min": 1,
+ "max": 120,
+ "defaultValue": 50,
+ "showIf": "enableLine1",
+ "group": "Appearance"
+ },
+ {
+ "id": "line1FontWeight",
+ "label": "Font Weight",
+ "description": "",
+ "type": "select",
+ "options": [
+ { "value": "100", "label": "Thin" },
+ { "value": "200", "label": "Extra Light" },
+ { "value": "300", "label": "Light" },
+ { "value": "400", "label": "Regular" },
+ { "value": "500", "label": "Medium" },
+ { "value": "600", "label": "Semi Bold" },
+ { "value": "700", "label": "Bold" },
+ { "value": "800", "label": "Extra Bold" },
+ { "value": "900", "label": "Black" }
+ ],
+ "defaultValue": "700",
+ "showIf": "enableLine1",
+ "group": "Appearance"
+ },
+ {
+ "id": "line1FontColor",
+ "label": "Font Color",
+ "description": "",
+ "type": "color",
+ "defaultValue": "#ffffff",
+ "showIf": "enableLine1",
+ "group": "Appearance"
+ },
+ {
+ "id": "line1FontOpacity",
+ "label": "Font Opacity",
+ "description": "",
+ "type": "number",
+ "defaultValue": "1",
+ "min": 0,
+ "max": 1,
+ "step": ".01",
+ "showIf": "enableLine1",
+ "group": "Appearance"
+ },
+ {
+ "id": "line1TextTransform",
+ "label": "Text Transform",
+ "description": "",
+ "type": "select",
+ "options": [
+ { "value": "none", "label": "Normal" },
+ { "value": "uppercase", "label": "Uppercase" },
+ { "value": "lowercase", "label": "Lowercase" },
+ { "value": "capitalize", "label": "Capitalize" }
+ ],
+ "defaultValue": "uppercase",
+ "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",
+ "description": "Show the second line of the clock",
+ "type": "checkbox",
+ "defaultValue": true,
+ "group": "Appearance"
+ },
+ {
+ "id": "line2Format",
+ "label": "Format",
+ "description": "Click here for formatting options",
+ "type": "text",
+ "defaultValue": "ddd, MMM D",
+ "showIf": "enableLine2",
+ "group": "Appearance"
+ },
+ {
+ "id": "line2FontSize",
+ "label": "Font Size",
+ "description": "",
+ "type": "number",
+ "min": 1,
+ "max": 120,
+ "defaultValue": 40,
+ "showIf": "enableLine2",
+ "group": "Appearance"
+ },
+ {
+ "id": "line2FontWeight",
+ "label": "Font Weight",
+ "description": "",
+ "type": "select",
+ "options": [
+ { "value": "100", "label": "Thin" },
+ { "value": "200", "label": "Extra Light" },
+ { "value": "300", "label": "Light" },
+ { "value": "400", "label": "Regular" },
+ { "value": "500", "label": "Medium" },
+ { "value": "600", "label": "Semi Bold" },
+ { "value": "700", "label": "Bold" },
+ { "value": "800", "label": "Extra Bold" },
+ { "value": "900", "label": "Black" }
+ ],
+ "defaultValue": "400",
+ "showIf": "enableLine2",
+ "group": "Appearance"
+ },
+ {
+ "id": "line2FontColor",
+ "label": "Font Color",
+ "description": "",
+ "type": "color",
+ "defaultValue": "#ffffff",
+ "showIf": "enableLine2",
+ "group": "Appearance"
+ },
+ {
+ "id": "line2FontOpacity",
+ "label": "Font Opacity",
+ "description": "",
+ "type": "number",
+ "defaultValue": "0.7",
+ "min": 0,
+ "max": 1,
+ "step": ".01",
+ "showIf": "enableLine2",
+ "group": "Appearance"
+ },
+ {
+ "id": "line2TextTransform",
+ "label": "Text Transform",
+ "description": "",
+ "type": "select",
+ "options": [
+ { "value": "none", "label": "Normal" },
+ { "value": "uppercase", "label": "Uppercase" },
+ { "value": "lowercase", "label": "Lowercase" },
+ { "value": "capitalize", "label": "Capitalize" }
+ ],
+ "defaultValue": "uppercase",
+ "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",
+ "description": "Show the third line of the clock",
+ "type": "checkbox",
+ "defaultValue": false,
+ "group": "Appearance"
+ },
+ {
+ "id": "line3Format",
+ "label": "Format",
+ "description": "Click here for formatting options",
+ "type": "text",
+ "defaultValue": "ddd DD MMM YYYY hh:mm:ss A z",
+ "showIf": "enableLine3",
+ "group": "Appearance"
+ },
+ {
+ "id": "line3FontSize",
+ "label": "Font Size",
+ "description": "",
+ "type": "number",
+ "min": 1,
+ "max": 120,
+ "defaultValue": 30,
+ "showIf": "enableLine3",
+ "group": "Appearance"
+ },
+ {
+ "id": "line3FontWeight",
+ "label": "Font Weight",
+ "description": "",
+ "type": "select",
+ "options": [
+ { "value": "100", "label": "Thin" },
+ { "value": "200", "label": "Extra Light" },
+ { "value": "300", "label": "Light" },
+ { "value": "400", "label": "Regular" },
+ { "value": "500", "label": "Medium" },
+ { "value": "600", "label": "Semi Bold" },
+ { "value": "700", "label": "Bold" },
+ { "value": "800", "label": "Extra Bold" },
+ { "value": "900", "label": "Black" }
+ ],
+ "defaultValue": "600",
+ "showIf": "enableLine3",
+ "group": "Appearance"
+ },
+ {
+ "id": "line3FontColor",
+ "label": "Font Color",
+ "description": "",
+ "type": "color",
+ "defaultValue": "#ffffff",
+ "showIf": "enableLine3",
+ "group": "Appearance"
+ },
+ {
+ "id": "line3FontOpacity",
+ "label": "Font Opacity",
+ "description": "",
+ "type": "number",
+ "defaultValue": "1",
+ "min": 0,
+ "max": 1,
+ "step": ".01",
+ "showIf": "enableLine3",
+ "group": "Appearance"
+ },
+ {
+ "id": "line3TextTransform",
+ "label": "Text Transform",
+ "description": "",
+ "type": "select",
+ "options": [
+ { "value": "none", "label": "Normal" },
+ { "value": "uppercase", "label": "Uppercase" },
+ { "value": "lowercase", "label": "Lowercase" },
+ { "value": "capitalize", "label": "Capitalize" }
+ ],
+ "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/clocko/style.css b/clocko/style.css
new file mode 100644
index 0000000..0615c04
--- /dev/null
+++ b/clocko/style.css
@@ -0,0 +1,32 @@
+* {
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
+ text-transform: uppercase;
+}
+
+#main-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ box-sizing: border-box;
+ height: 95vh;
+ padding: 1em;
+}
+
+.timeLabel {
+ font-size: 40px;
+ /* text-shadow: rgb(0, 0, 0) 2px 2px 2px; */
+ color: white;
+}
+
+#line1 {
+ text-align: center;
+}
+
+#line2 {
+ text-align: right;
+}
\ No newline at end of file
diff --git a/horizontal-chat/script.js b/horizontal-chat/script.js
index f5fc40f..e7446eb 100644
--- a/horizontal-chat/script.js
+++ b/horizontal-chat/script.js
@@ -97,7 +97,8 @@ let youtubeUsername = urlParams.get("youtubeUsername") || "";
////////////////
// Set fonts for the widget
-document.body.style.fontFamily = font;
+if (font)
+ document.body.style.fontFamily = `'${font}'`;
document.body.style.fontSize = `${fontSize}px`;
// Set the background color
diff --git a/horizontal-chat/settings/settings.json b/horizontal-chat/settings/settings.json
index a984361..7ecd4b6 100644
--- a/horizontal-chat/settings/settings.json
+++ b/horizontal-chat/settings/settings.json
@@ -59,8 +59,8 @@
{
"id": "font",
"label": "Font",
- "description": "",
- "type": "text",
+ "description": "Check installed fonts",
+ "type": "font",
"defaultValue": "",
"group": "Appearance"
},
diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js
index 49afc56..41776d2 100644
--- a/multichat-overlay/script.js
+++ b/multichat-overlay/script.js
@@ -104,7 +104,8 @@ let youtubeUsername = urlParams.get("youtubeUsername") || "";
////////////////
// Set fonts for the widget
-document.body.style.fontFamily = font;
+if (font)
+ document.body.style.fontFamily = `'${font}'`;
document.body.style.fontSize = `${fontSize}px`;
// Set line spacing
diff --git a/multichat-overlay/settings/settings.json b/multichat-overlay/settings/settings.json
index 6c53129..3c73b97 100644
--- a/multichat-overlay/settings/settings.json
+++ b/multichat-overlay/settings/settings.json
@@ -59,8 +59,8 @@
{
"id": "font",
"label": "Font",
- "description": "",
- "type": "text",
+ "description": "Check installed fonts",
+ "type": "font",
"defaultValue": "",
"group": "Appearance"
},
diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js
index f141c4b..412dcef 100644
--- a/multistream-alerts/script.js
+++ b/multistream-alerts/script.js
@@ -129,7 +129,8 @@ if (!showAvatar) {
}
// Set fonts for the widget
-document.body.style.fontFamily = font;
+if (font)
+ document.body.style.fontFamily = `'${font}'`;
document.body.style.fontSize = `${fontSize}px`;
document.body.style.color = fontColor;
@@ -255,6 +256,11 @@ client.on('YouTube.GiftMembershipReceived', (response) => {
YouTubeGiftMembershipReceived(response.data);
})
+client.on('Kick.Follow', (response) => {
+ console.debug(response.data);
+ KickFollow(response.data);
+})
+
client.on('Streamlabs.Donation', (response) => {
console.debug(response.data);
StreamlabsDonation(response.data);
diff --git a/multistream-alerts/settings/settings.json b/multistream-alerts/settings/settings.json
index c919d79..49bd7f9 100644
--- a/multistream-alerts/settings/settings.json
+++ b/multistream-alerts/settings/settings.json
@@ -19,8 +19,8 @@
{
"id": "font",
"label": "Font",
- "description": "",
- "type": "text",
+ "description": "Check installed fonts",
+ "type": "font",
"defaultValue": "",
"group": "Appearance"
},
diff --git a/now-playing/images/placeholder.af b/now-playing/images/placeholder.af
new file mode 100644
index 0000000..6eaf7cd
Binary files /dev/null and b/now-playing/images/placeholder.af differ
diff --git a/now-playing/images/placeholder.png b/now-playing/images/placeholder.png
new file mode 100644
index 0000000..db87688
Binary files /dev/null and b/now-playing/images/placeholder.png differ
diff --git a/now-playing/index.html b/now-playing/index.html
new file mode 100644
index 0000000..3a7bd47
--- /dev/null
+++ b/now-playing/index.html
@@ -0,0 +1,20 @@
+
+
+
+ Now Playing
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/now-playing/script.js b/now-playing/script.js
new file mode 100644
index 0000000..64aed80
--- /dev/null
+++ b/now-playing/script.js
@@ -0,0 +1,526 @@
+////////////////
+// PARAMETERS //
+////////////////
+
+const queryString = window.location.search;
+const urlParams = new URLSearchParams(queryString);
+
+////////////////
+// CONSTANTS //
+////////////////
+
+const REQUIRED_VERSION = '0.0.4';
+const SMTC_BRIDGE_DOWNLOAD_URL = 'https://github.com/nuttylmao/smtc-bridge/releases';
+let VersionChecked = false;
+
+///////////////////
+// PAGE ELEMENTS //
+///////////////////
+
+const mainContainer = document.getElementById('main-container');
+
+/////////////
+// OPTIONS //
+/////////////
+
+const smtcBridgeAddress = urlParams.get('smtcBridgeAddress') || '127.0.0.1';
+const smtcBridgePort = urlParams.get('smtcBridgePort') || '5000';
+
+const theme = urlParams.get('theme') || 'standard';
+const font = urlParams.get("font") || "";
+const fontSize = GetIntParam("fontSize", 20);
+const maxWidth = GetIntParam("maxWidth", 500);
+const verticalAlignment = urlParams.get('verticalAlignment') || 'align-to-center';
+const textAlignment = urlParams.get('textAlignment') || 'left';
+const useCustomColors = GetBooleanParam("useCustomColors", false);
+const color1 = urlParams.get('color1') || '#ffffff';
+const color2 = urlParams.get('color2') || '#1d1d1d';
+
+const includedApplications = urlParams.get('includedApplications') || '';
+const excludedApplications = urlParams.get('excludedApplications') || '';
+const showAlbumArt = GetBooleanParam("showAlbumArt", true);
+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';
+
+
+
+/////////////////
+// GLOBAL VARS //
+/////////////////
+
+let smtcBridgePopup = null;
+let versionCheckPopup = null;
+let errorPopup = null;
+let skipVersionCheck = false;
+let CurrentPlaybackStatus;
+let CurrentSongKey;
+let hideTimeout = null;
+
+////////////////
+// PAGE SETUP //
+////////////////
+
+// Set fonts for the widget
+if (font)
+ document.body.style.fontFamily = `'${font}'`;
+document.body.style.fontSize = `${fontSize}px`;
+
+// Set album art visibility
+if (showAlbumArt)
+ document.documentElement.style.setProperty('--show-album-art', ``);
+else
+ document.documentElement.style.setProperty('--show-album-art', `none`);
+
+// Set text alignment
+document.documentElement.style.setProperty('--text-alignment', `${textAlignment}`);
+
+// Set vertical alignment
+switch (verticalAlignment) {
+ case 'align-to-top':
+ mainContainer.style.alignItems = 'flex-start';
+ break;
+ case 'align-to-center':
+ mainContainer.style.alignItems = 'center';
+ break;
+ case 'align-to-bottom':
+ mainContainer.style.alignItems = 'flex-end';
+ break;
+}
+
+
+
+////////////////
+// LOAD THEME //
+////////////////
+
+// Load the theme
+async function LoadTheme()
+{
+ // Check if the theme inherits from a base theme
+ let baseTheme = theme;
+ try {
+ const response = await fetch(`./themes/${theme}/inherits.json`);
+ if (response.ok) {
+ const config = await response.json();
+ baseTheme = config['base-theme'];
+ }
+ } catch (e) {
+ // No inheritance file found, assume it is a base theme
+ }
+
+ // Fetch the HTML structure from the base theme folder
+ const response = await fetch(`./themes/${baseTheme}/index.html`);
+ const html = await response.text();
+
+ mainContainer.innerHTML = html;
+
+ // Swap the CSS file
+ const link = document.getElementById('theme-style');
+ link.href = `./themes/${baseTheme}/style.css`;
+
+ // Load the theme-specific JS
+ // We remove the old script tag and add a new one
+ const oldScript = document.getElementById('theme-script');
+ if (oldScript) oldScript.remove();
+
+ const script = document.createElement('script');
+ script.src = `./themes/${baseTheme}/script.js`;
+ script.id = 'theme-script';
+ document.body.appendChild(script);
+
+ // If the base theme != theme, that means this is a variant, so load the variant's CSS
+ if (baseTheme != theme)
+ {
+ // Load the variant CSS
+ const baseLink = document.createElement('link');
+ baseLink.rel = 'stylesheet';
+ baseLink.className = 'theme-style'; // Use class for easy batch removal
+ baseLink.href = `./themes/${theme}/style.css`;
+ document.head.appendChild(baseLink);
+
+ window.ThemeVariant = theme;
+ }
+}
+
+LoadTheme();
+
+
+
+/////////////////
+// NOW PLAYING //
+/////////////////
+
+async function FetchMedia() {
+ try {
+ const response = await fetch(`http://${smtcBridgeAddress}:${smtcBridgePort}/now-playing`);
+ const data = await response.json();
+
+ // Remove the SMTC Bridge popup if it's on screen
+ CloseSMTCBridgePopup();
+
+ // Check for errors in the response
+ if (data.error) {
+ ShowErrorPopup(data.error);
+ return;
+ }
+ else {
+ CloseErrorPopup();
+ }
+
+ // Check the SMTC Bridge version
+ if (!VersionChecked) {
+ CheckSMTCBridgeVersion(data.app_version);
+ VersionChecked = true;
+ }
+
+ // Update the UI with the received data
+ // console.log(data);
+ UpdatePlayerState(data);
+ } catch (error) {
+ console.error("Failed to connect to Flask media server:", error);
+
+ // Show a popup to instruct the user to install SMTC Bridge
+ ShowWaitingForSMTCBridgePopup();
+ }
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+ setTimeout(() => {
+ // Start polling every 1000 milliseconds
+ setInterval(FetchMedia, 1000);
+
+ // Run once immediately on script load
+ FetchMedia();
+ }, 100);
+});
+
+
+
+////////////
+// POPUPS //
+////////////
+
+function ShowWaitingForSMTCBridgePopup() {
+ const newPopup = SplashscreenPopup(
+ '/.common/resources/smtc-bridge-icon.png',
+ 'Waiting for SMTC Bridge',
+ 'Please launch SMTC Bridge',
+ '', // Attribute text
+ 'linear-gradient(0deg, #111111 0%, #11001f 100%)',
+ {
+ text: 'Download',
+ action: () => {
+ window.open(SMTC_BRIDGE_DOWNLOAD_URL, "_blank");
+ }
+ }
+ );
+
+ if (newPopup) {
+ smtcBridgePopup = newPopup;
+ }
+}
+
+
+function ShowSMTCBridgeUpdateRequiredPopup(installedVersion) {
+ const newPopup = SplashscreenPopup(
+ '/.common/resources/smtc-bridge-icon.png',
+ 'Update Required',
+ 'Your version of SMTC Bridge is out of date.',
+ `Installed Version: ${installedVersion}
New Version: ${REQUIRED_VERSION}`,
+ 'linear-gradient(0deg, #1b0005 0%, #4f000d 100%)',
+ {
+ text: 'Download',
+ action: () => {
+ window.open(SMTC_BRIDGE_DOWNLOAD_URL, "_blank");
+ }
+ }
+ );
+
+ if (newPopup) {
+ versionCheckPopup = newPopup;
+ }
+}
+
+function ShowSMTCBridgeUpdateAvailablePopup(installedVersion) {
+ const newPopup = SplashscreenPopup(
+ '/.common/resources/smtc-bridge-icon.png',
+ 'Update Available',
+ 'A new version of SMTC Bridge is available.',
+ `Installed Version: ${installedVersion}
New Version: ${REQUIRED_VERSION}`,
+ 'linear-gradient(0deg, #2a004f 0%, #4f2675 100%)',
+ {
+ text: 'Download',
+ action: () => {
+ window.open(SMTC_BRIDGE_DOWNLOAD_URL, "_blank");
+ }
+ }
+ );
+
+ if (newPopup) {
+ versionCheckPopup = newPopup;
+ }
+
+ setTimeout(() => {
+ CloseVersionCheckPopup();
+ }, 10000);
+}
+
+function ShowErrorPopup(errorMessage) {
+ const newPopup = SplashscreenPopup(
+ '/.common/resources/smtc-bridge-icon.png',
+ 'SMTC Bridge Error',
+ `I'm a shit programmer and I fucked something up.`,
+ `Error: ${errorMessage}`,
+ 'linear-gradient(0deg, #1b0005 0%, #4f000d 100%)'
+ );
+
+ if (newPopup) {
+ errorPopup = newPopup;
+ }
+}
+
+function CloseErrorPopup() {
+ if (errorPopup) {
+ errorPopup.close();
+ errorPopup = null;
+ }
+}
+
+function CloseSMTCBridgePopup()
+{
+ if (smtcBridgePopup) {
+ smtcBridgePopup.close();
+ smtcBridgePopup = null;
+ }
+}
+
+function CloseVersionCheckPopup()
+{
+ if (versionCheckPopup) {
+ versionCheckPopup.close();
+ versionCheckPopup = null;
+ }
+}
+
+//////////////////////
+// HELPER FUNCTIONS //
+//////////////////////
+
+function CheckSMTCBridgeVersion(installedVersion) {
+ // Check that the server/client are on the same SMTC Bridge version
+ // const VersionStatus = VersionCheck(REQUIRED_VERSION, installedVersion);
+ const versionStatus = VersionCheck(REQUIRED_VERSION, installedVersion);
+
+ if (skipVersionCheck)
+ return;
+
+ switch (versionStatus)
+ {
+ case 'incompatible':
+ ShowSMTCBridgeUpdateRequiredPopup(installedVersion);
+ skipVersionCheck = false;
+ break;
+ case 'soft-warning':
+ ShowSMTCBridgeUpdateAvailablePopup(installedVersion);
+ skipVersionCheck = true;
+ break;
+ default:
+ CloseVersionCheckPopup();
+ break;
+ }
+}
+
+// Each theme must implement the following
+async function UpdatePlayerState(data) {
+ // Parse and clean settings arrays
+ const includedList = includedApplications
+ ? includedApplications.split(',').map(app => app.trim().toLowerCase()).filter(Boolean)
+ : [];
+
+ const excludedList = excludedApplications
+ ? excludedApplications.split(',').map(app => app.trim().toLowerCase()).filter(Boolean)
+ : [];
+
+ // Filter out any sessions belonging to excluded apps
+ const validSessions = data.sessions.filter(s => {
+ const appId = (s.source_app_id || "").toLowerCase();
+ // Check if the source app ID matches any exclusion entry
+ const isExcluded = excludedList.some(excluded => appId.includes(excluded));
+ return !isExcluded;
+ });
+
+ let targetSession = null;
+
+ // Priority Check: If user specified included apps, hunt for them in exact order
+ if (includedList.length > 0) {
+ // Step 1: Look through the included list for an app that is CURRENTLY PLAYING
+ 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;
+ return matchesApp && isPlaying;
+ });
+ if (targetSession) break;
+ }
+
+ // Step 2: If none of the included apps are playing, fall back to ANY session in the included list (even if paused)
+ if (!targetSession) {
+ for (const targetApp of includedList) {
+ targetSession = validSessions.find(s =>
+ (s.source_app_id || "").toLowerCase().includes(targetApp)
+ );
+ if (targetSession) break;
+ }
+ }
+ } else {
+ // Fallback when no included list is provided:
+ // Priority 1: Check Windows' current focused session ID first
+ if (data.current_session_id) {
+ targetSession = validSessions.find(s => s.source_app_id === data.current_session_id);
+ }
+
+ // 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);
+ if (playingSession) {
+ targetSession = playingSession;
+ }
+ }
+
+ // Priority 3: Ultimate fallback to the first valid session available if nothing else matched
+ if (!targetSession && validSessions.length > 0) {
+ targetSession = validSessions[0];
+ }
+ }
+
+ // If a target session was found, update the state of the widget
+ if (targetSession) {
+ // Extract the relevant properties from the session
+ const playbackInfo = targetSession.playback_info;
+ const mediaProps = targetSession.media_properties;
+ const timelineProps = targetSession.timeline_properties;
+
+ // Calcualte an accent color
+ const accentColorPalette = await GetAccentPalette(mediaProps.Thumbnail ?? './images/placeholder.png');
+
+ // 1. Check if playback status has changed and update visibility accordingly
+ if (playbackInfo.PlaybackStatus !== CurrentPlaybackStatus) {
+ if (playbackInfo.PlaybackStatus === PlaybackStatus.PLAYING)
+ SetVisibility(true);
+ else
+ SetVisibility(false);
+ CurrentPlaybackStatus = playbackInfo.PlaybackStatus;
+ }
+
+
+ // 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) {
+ const newTrackKey = `${mediaProps.Title}-${mediaProps.Artist}-${mediaProps.Thumbnail}`;
+ if (newTrackKey !== CurrentSongKey) {
+ ChangeTrack(mediaProps, accentColorPalette); // Now trigger your cross-fade logic here!
+ CurrentSongKey = newTrackKey; // Update the tracker with the string key
+ }
+ }
+
+ // 3. Update the progress info
+ if (timelineProps) {
+ // Parse the Windows timestamp into a JavaScript time object
+ const lastUpdateAnchor = Date.parse(timelineProps.LastUpdatedTime.replace(' ', 'T'));
+
+ // Calculate the drift (i.e. how many milliseconds have passed since Windows last updated)
+ const driftMs = Date.now() - lastUpdateAnchor;
+
+ // Add that drift to the reported Position
+ // Only add drift if the status is PLAYING
+ const isPlaying = (targetSession.playback_info.PlaybackStatus === PlaybackStatus.PLAYING);
+ const currentPositionMs = isPlaying && timelineProps.EndTime > 0 ? timelineProps.Position + driftMs : timelineProps.Position;
+
+ SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette);
+ }
+ }
+ else {
+ SetVisibility(false);
+ }
+}
+
+function SetVisibility(visible) {
+ // Always clear any pending hide timers whenever we change visibility
+ if (hideTimeout) {
+ clearTimeout(hideTimeout);
+ hideTimeout = null;
+ }
+
+ if (visible) {
+ mainWrapper.style.animation = `${showAnimation} 0.5s ease-out forwards`;
+
+ // Only set a new timer if autoHide is enabled
+ if (autoHide) {
+ hideTimeout = setTimeout(() => {
+ SetVisibility(false);
+ }, displayDuration * 1000);
+ }
+ } else {
+ mainWrapper.style.animation = `${hideAnimation} 0.5s ease-out forwards`;
+ }
+}
+
+// MARQUEE LOGIC
+// This is a more advanced marquee implementation that calculates the overflow distance and adjusts the scroll speed accordingly
+const labelData = new Map();
+
+// Adjust this value to change scroll speed (pixels per second)
+const SCROLL_SPEED_PX_PER_SEC = 40;
+
+function SetLabelText(elementId, text) {
+ const label = document.getElementById(elementId);
+ if (!label) return;
+
+ labelData.set(elementId, text);
+ ApplyMarquee(label, text);
+
+ // Keep it responsive on container resize
+ if (!label._resizeObserver) {
+ label._resizeObserver = new ResizeObserver(() => {
+ const currentText = labelData.get(elementId);
+ if (currentText) ApplyMarquee(label, currentText);
+ });
+ label._resizeObserver.observe(label);
+ }
+}
+
+function ApplyMarquee(label, text) {
+ // 1. Render single span to measure dimensions
+ label.classList.remove('is-overflowing');
+ label.innerHTML = ``;
+ label.querySelector('.scroll-content').textContent = text;
+
+ const scrollContent = label.querySelector('.scroll-content');
+
+ const textWidth = scrollContent.scrollWidth;
+ const containerWidth = label.clientWidth;
+ const overflowDistance = textWidth - containerWidth;
+
+ // 2. Check if text exceeds container
+ if (overflowDistance > 0) {
+ // Calculate travel distance in percentage relative to scrollContent's width
+ const distancePercent = (overflowDistance / textWidth) * 100;
+
+ // Compute duration to keep speed consistent regardless of length
+ // We multiply travel time by 2 (for both directions) plus 3 seconds for pauses
+ const travelTime = overflowDistance / SCROLL_SPEED_PX_PER_SEC;
+ const totalDuration = (travelTime * 2) + 3;
+
+ // Pass calculated variables to CSS
+ scrollContent.style.setProperty('--scroll-distance', `-${distancePercent}%`);
+ label.style.setProperty('--marquee-duration', `${totalDuration}s`);
+
+ label.classList.add('is-overflowing');
+ }
+}
\ No newline at end of file
diff --git a/now-playing/settings/index.html b/now-playing/settings/index.html
new file mode 100644
index 0000000..2fd0841
--- /dev/null
+++ b/now-playing/settings/index.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ nutty
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/now-playing/settings/script.js b/now-playing/settings/script.js
new file mode 100644
index 0000000..f067f52
--- /dev/null
+++ b/now-playing/settings/script.js
@@ -0,0 +1,26 @@
+const widgetContainer = document.getElementById('widgetContainer');
+
+const settingsPageURL = '/.common/core/settings-core';
+
+const currentURL = window.location.href;
+
+let settingsJSON;
+let baseURL = currentURL;
+
+if (baseURL.endsWith("index.html"))
+ baseURL = baseURL.replace("index.html", "");
+
+settingsJSON = "?settingsJson=" + baseURL + "settings.json";
+
+const lastSlashIndex = baseURL.lastIndexOf("/");
+let widgetURL = "&widgetURL=" + baseURL.replace("/settings", "");
+
+const usesStreamerBot = "&usesStreamerBot=false";
+
+console.debug("Window Ref: " + window.location.href);
+console.debug("Base URL: " + baseURL);
+console.debug("Settings JSON: " + settingsJSON);
+console.debug("Widget URL: " + widgetURL);
+console.debug("Uses Streamer Bot: " + usesStreamerBot);
+
+widgetContainer.src = settingsPageURL + settingsJSON + widgetURL + usesStreamerBot;
\ No newline at end of file
diff --git a/now-playing/settings/settings.json b/now-playing/settings/settings.json
new file mode 100644
index 0000000..ee532ca
--- /dev/null
+++ b/now-playing/settings/settings.json
@@ -0,0 +1,307 @@
+{
+ "settings": [
+ {
+ "id": "theme",
+ "label": "Theme",
+ "description": "",
+ "type": "select",
+ "options": [
+ {
+ "value": "standard",
+ "label": "Standard"
+ },
+ {
+ "value": "matte",
+ "label": "Matte (Light)"
+ },
+ {
+ "value": "matte-dark",
+ "label": "Matte (Dark)"
+ },
+ {
+ "value": "compact",
+ "label": "Compact"
+ },
+ {
+ "value": "compact-inverted",
+ "label": "Compact (Inverted)"
+ },
+ {
+ "value": "simple",
+ "label": "Simple"
+ },
+ {
+ "value": "classic",
+ "label": "Classic"
+ },
+ {
+ "value": "card",
+ "label": "Card"
+ },
+ {
+ "value": "album-art",
+ "label": "Album Art"
+ },
+ {
+ "value": "vinyl",
+ "label": "Vinyl"
+ },
+ {
+ "value": "color-palette",
+ "label": "Color Palette"
+ }
+ ],
+ "defaultValue": "standard",
+ "group": "Appearance"
+ },
+ {
+ "id": "font",
+ "label": "Font",
+ "description": "Check installed fonts",
+ "type": "font",
+ "defaultValue": "",
+ "group": "Appearance"
+ },
+ {
+ "id": "fontSize",
+ "label": "Font Size",
+ "description": "",
+ "type": "number",
+ "min": 0,
+ "defaultValue": 20,
+ "group": "Appearance"
+ },
+ {
+ "id": "maxWidth",
+ "label": "Max Width",
+ "description": "Set to 0 to utilize full browser width",
+ "type": "number",
+ "min": 0,
+ "defaultValue": 500,
+ "group": "Appearance"
+ },
+ {
+ "id": "verticalAlignment",
+ "label": "Vertical Alignment",
+ "description": "",
+ "type": "select",
+ "options": [
+ {
+ "value": "align-to-top",
+ "label": "Align To Top"
+ },
+ {
+ "value": "align-to-center",
+ "label": "Align To Center"
+ },
+ {
+ "value": "align-to-bottom",
+ "label": "Align To Bottom"
+ }
+ ],
+ "defaultValue": "align-to-center",
+ "group": "Appearance"
+ },
+ {
+ "id": "textAlignment",
+ "label": "Text Alignment",
+ "description": "",
+ "type": "select",
+ "options": [
+ {
+ "value": "left",
+ "label": "Left"
+ },
+ {
+ "value": "center",
+ "label": "Center"
+ },
+ {
+ "value": "right",
+ "label": "Right"
+ }
+ ],
+ "defaultValue": "left",
+ "group": "Appearance"
+ },
+ {
+ "id": "useCustomColors",
+ "label": "Use Custom Colors",
+ "description": "",
+ "type": "checkbox",
+ "defaultValue": false,
+ "group": "Appearance"
+ },
+ {
+ "id": "color1",
+ "label": "Primary Color",
+ "description": "",
+ "type": "color",
+ "defaultValue": "#ffffff",
+ "showIf": "useCustomColors",
+ "group": "Appearance"
+ },
+ {
+ "id": "color2",
+ "label": "Secondary Color",
+ "description": "",
+ "type": "color",
+ "defaultValue": "#1d1d1d",
+ "showIf": "useCustomColors",
+ "group": "Appearance"
+ },
+ {
+ "id": "includedApplications",
+ "label": "Included Apps",
+ "description": "List of apps to display, in order of priority.
Leave empty to track currently focused app.
View active sources",
+ "type": "text",
+ "placeholder": "Spotify.exe, vlc.exe",
+ "defaultValue": "",
+ "group": "General"
+ },
+ {
+ "id": "excludedApplications",
+ "label": "Excluded Apps",
+ "description": "Apps listed here will not be displayed.
View active sources",
+ "type": "text",
+ "placeholder": "Chrome, vlc.exe",
+ "defaultValue": "",
+ "group": "General"
+ },
+ {
+ "id": "showAlbumArt",
+ "label": "Show Album Art",
+ "description": "",
+ "type": "checkbox",
+ "defaultValue": true,
+ "group": "General"
+ },
+ {
+ "id": "showProgressBar",
+ "label": "Show Progress Bar",
+ "description": "Some apps do not display progress info correctly.",
+ "type": "checkbox",
+ "defaultValue": true,
+ "group": "General"
+ },
+ {
+ "id": "swapArtistTrack",
+ "label": "Swap Artist and Track",
+ "description": "Displays Artist above Track instead of Track above Artist.",
+ "type": "checkbox",
+ "defaultValue": false,
+ "group": "General"
+ },
+ {
+ "id": "showPrimary",
+ "label": "Show Primary Text",
+ "description": "",
+ "type": "checkbox",
+ "defaultValue": true,
+ "group": "General"
+ },
+ {
+ "id": "showSecondary",
+ "label": "Show Secondary Text",
+ "description": "",
+ "type": "checkbox",
+ "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",
+ "description": "",
+ "type": "select",
+ "options": [
+ {
+ "value": "fade-in",
+ "label": "Fade In"
+ },
+ {
+ "value": "slide-in-from-top",
+ "label": "Slide In From Top"
+ },
+ {
+ "value": "slide-in-from-bottom",
+ "label": "Slide In From Bottom"
+ },
+ {
+ "value": "slide-in-from-left",
+ "label": "Slide In From Left"
+ },
+ {
+ "value": "slide-in-from-right",
+ "label": "Slide In From Right"
+ }
+ ],
+ "defaultValue": "slide-in-from-bottom",
+ "group": "General"
+ },
+ {
+ "id": "hideAnimation",
+ "label": "Hide Animation",
+ "description": "",
+ "type": "select",
+ "options": [
+ {
+ "value": "fade-out",
+ "label": "Fade Out"
+ },
+ {
+ "value": "slide-out-top",
+ "label": "Slide Out Top"
+ },
+ {
+ "value": "slide-out-bottom",
+ "label": "Slide Out Down"
+ },
+ {
+ "value": "slide-out-left",
+ "label": "Slide Out Left"
+ },
+ {
+ "value": "slide-out-right",
+ "label": "Slide Out Right"
+ }
+ ],
+ "defaultValue": "slide-out-bottom",
+ "group": "General"
+ },
+ {
+ "id": "smtcBridgeAddress",
+ "label": "Address",
+ "description": "",
+ "type": "text",
+ "defaultValue": "127.0.0.1",
+ "group": "SMTC Bridge"
+ },
+ {
+ "id": "smtcBridgePort",
+ "label": "Port",
+ "description": "",
+ "type": "text",
+ "defaultValue": "5000",
+ "group": "SMTC Bridge"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/now-playing/style.css b/now-playing/style.css
new file mode 100644
index 0000000..1d6282c
--- /dev/null
+++ b/now-playing/style.css
@@ -0,0 +1,252 @@
+body {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
+ font-size: 20px;
+ color: white;
+ margin: 0;
+}
+
+#main-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ padding: 1em;
+ box-sizing: border-box;
+}
+
+
+@keyframes fade-in {
+ 0% {
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+}
+
+@keyframes fade-out {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ }
+}
+
+@keyframes slide-in-from-top {
+ 0% {
+ transform: translateY(-1em);
+ opacity: 0;
+ }
+
+ 100% {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+@keyframes slide-in-from-top {
+ 0% {
+ transform: translateY(-1em);
+ opacity: 0;
+ }
+
+ 100% {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+@keyframes slide-out-top {
+ 0% {
+ transform: translateY(0);
+ opacity: 1;
+ }
+
+ 100% {
+ transform: translateY(-1em);
+ opacity: 0;
+ }
+}
+
+@keyframes slide-in-from-bottom {
+ 0% {
+ transform: translateY(1em);
+ opacity: 0;
+ }
+
+ 100% {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+@keyframes slide-out-bottom {
+ 0% {
+ transform: translateY(0);
+ opacity: 1;
+ }
+
+ 100% {
+ transform: translateY(1em);
+ opacity: 0;
+ }
+}
+
+@keyframes slide-in-from-left {
+ 0% {
+ transform: translateX(-1em);
+ opacity: 0;
+ }
+
+ 100% {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+@keyframes slide-out-left {
+ 0% {
+ transform: translateX(0);
+ opacity: 1;
+ }
+
+ 100% {
+ transform: translateX(-1em);
+ opacity: 0;
+ }
+}
+
+@keyframes slide-in-from-right {
+ 0% {
+ transform: translateX(1em);
+ opacity: 0;
+ }
+
+ 100% {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+@keyframes slide-out-right {
+ 0% {
+ transform: translateX(0);
+ opacity: 1;
+ }
+
+ 100% {
+ transform: translateX(1em);
+ opacity: 0;
+ }
+}
+
+
+
+@property --mask-left {
+ syntax: '';
+ inherits: false;
+ initial-value: black;
+}
+
+@property --mask-right {
+ syntax: '';
+ inherits: false;
+ initial-value: transparent;
+}
+
+.text-label {
+ --fade-width: 0.5em;
+ white-space: nowrap;
+ overflow: hidden;
+ display: block;
+ width: 100%;
+ mask-image: var(--trailing-fade);
+ -webkit-mask-image: var(--trailing-fade);
+ transition: opacity 0.25s ease-in-out;
+}
+
+.text-label:empty {
+ display: none !important;
+}
+
+.text-label:has(.scroll-content:empty),
+.text-label:has(#track-label:empty) {
+ display: none !important;
+}
+
+.text-label .scroll-content {
+ display: inline-flex;
+ white-space: nowrap;
+ will-change: transform;
+}
+
+.text-label.is-overflowing .scroll-content {
+ animation: ping-pong-scroll var(--marquee-duration, 8s) ease-in-out infinite;
+}
+
+.text-label.is-overflowing {
+ animation: ping-pong-mask var(--marquee-duration, 8s) ease-in-out infinite;
+
+ /* Static gradient structure referencing dynamic color variables */
+ mask-image: linear-gradient(
+ to right,
+ var(--mask-left) 0px,
+ black var(--fade-width),
+ black calc(100% - var(--fade-width)),
+ var(--mask-right) 100%
+ );
+ -webkit-mask-image: linear-gradient(
+ to right,
+ var(--mask-left) 0px,
+ black var(--fade-width),
+ black calc(100% - var(--fade-width)),
+ var(--mask-right) 100%
+ );
+}
+
+@keyframes ping-pong-scroll {
+ 0%, 15% {
+ transform: translateX(0%);
+ }
+ 50%, 65% {
+ transform: translateX(var(--scroll-distance, -100%));
+ }
+ 100% {
+ transform: translateX(0%);
+ }
+}
+
+@keyframes ping-pong-mask {
+ /* 1. AT START: Left is solid (black), Right is faded (transparent) */
+ 0%, 15% {
+ --mask-left: black;
+ --mask-right: transparent;
+ }
+
+ /* 2. MOVING LEFT: Left smoothly fades in (transparent), Right stays faded */
+ 22%, 43% {
+ --mask-left: transparent;
+ --mask-right: transparent;
+ }
+
+ /* 3. AT FAR END: Right smoothly solidifies (black), Left stays faded */
+ 50%, 65% {
+ --mask-left: transparent;
+ --mask-right: black;
+ }
+
+ /* 4. MOVING RIGHT: Right smoothly fades back out (transparent) */
+ 72%, 93% {
+ --mask-left: transparent;
+ --mask-right: transparent;
+ }
+
+ /* 5. RETURN TO START: Left smoothly solidifies (black) */
+ 100% {
+ --mask-left: black;
+ --mask-right: transparent;
+ }
+}
\ No newline at end of file
diff --git a/now-playing/themes/album-art/index.html b/now-playing/themes/album-art/index.html
new file mode 100644
index 0000000..bfac360
--- /dev/null
+++ b/now-playing/themes/album-art/index.html
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/now-playing/themes/album-art/script.js b/now-playing/themes/album-art/script.js
new file mode 100644
index 0000000..e5a82e4
--- /dev/null
+++ b/now-playing/themes/album-art/script.js
@@ -0,0 +1,111 @@
+///////////////////
+// PAGE ELEMENTS //
+///////////////////
+
+const mainWrapper = document.getElementById('main-wrapper');
+const songInfoContainer = document.getElementById('song-info-container');
+const albumArtContainer = document.getElementById('album-art-container');
+const albumArtLayer = document.getElementById('album-art-layer');
+const albumArtTransition = document.getElementById('album-art-transition-layer');
+const textInfoContainer = document.getElementById('text-info-container');
+const trackLabel = document.getElementById('track-label');
+const artistLabel = document.getElementById('artist-label');
+const progressBar = document.getElementById('progress-bar');
+
+
+
+////////////////
+// PAGE SETUP //
+////////////////
+
+// Set property visibility
+trackLabel.style.display = showPrimary ? '' : 'none';
+artistLabel.style.display = showSecondary ? '' : 'none';
+
+// Set container width
+if (maxWidth > 0)
+ mainWrapper.style.width = `${maxWidth}px`;
+else
+ mainWrapper.style.width = `100%`;
+
+mainWrapper.style.height = `${mainWrapper.style.clientWidth}px`;
+
+// Set progress bar visibility
+if (!showProgressBar)
+ progressBar.style.display = 'none';
+
+
+
+////////////////////
+// CORE FUNCTIONS //
+////////////////////
+
+async function ChangeTrack(mediaProps, accentColorPalette) {
+ // Fade in the overlay (shows the new text)
+ if (trackLabel.innerText != (swapArtistTrack ? mediaProps.Artist : mediaProps.Title))
+ trackLabel.style.opacity = "0";
+ if (artistLabel.innerText != (swapArtistTrack ? mediaProps.Title : mediaProps.Artist))
+ artistLabel.style.opacity = "0";
+ albumArtLayer.style.opacity = "0";
+
+ // Wait for fade (0.5s), then swap the real text and hide overlay
+ setTimeout(() => {
+ SetLabelText('track-label', swapArtistTrack ? mediaProps.Artist : mediaProps.Title);
+ SetLabelText('artist-label', swapArtistTrack ? mediaProps.Title : mediaProps.Artist);
+
+ // Extract the image source string (use fallback if Windows has no art)
+ const newArtUrl = mediaProps.Thumbnail ?? './images/placeholder.png';
+
+ // Set the image
+ albumArtLayer.style.backgroundImage = `url('${newArtUrl}')`;
+
+ // Apply a tint
+ if (!useCustomColors)
+ {
+ const baseColor = accentColorPalette.DarkMuted;
+ // Solid from 0% to 30%, then fading to 0% opacity at 100%
+ textInfoContainer.style.background = `linear-gradient(to top, ${baseColor}FF 0%, ${baseColor}FF 10%, ${baseColor}00 100%)`;
+ if (!showAlbumArt)
+ songInfoContainer.style.background = accentColorPalette.LightVibrant;
+
+ // Apply text color
+ document.body.style.color = accentColorPalette.LightVibrant;
+ }
+ else
+ {
+ const baseColor = color2;
+ // Solid from 0% to 30%, then fading to 0% opacity at 100%
+ textInfoContainer.style.background = `linear-gradient(to top, ${baseColor}FF 0%, ${baseColor}FF 10%, ${baseColor}00 100%)`;
+ if (!showAlbumArt)
+ songInfoContainer.style.background = color1;
+
+ // Apply text color
+ document.body.style.color = color1;
+ }
+
+ trackLabel.style.opacity = "";
+ artistLabel.style.opacity = "";
+ albumArtLayer.style.opacity = "";
+
+ setTimeout(() => {
+ // Set the image
+ albumArtTransition.style.backgroundImage = `url('${newArtUrl}')`;
+
+ SetVisibility(true); // Show the overlay for a few seconds if autoHide is enabled
+ }, 250);
+ }, 250);
+}
+
+function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
+
+ // Set progressbar
+ // Ensure we don't divide by zero or exceed 100%
+ const durationMs = timelineProps.EndTime;
+ let progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0;
+ progressPercent = Math.min(100, Math.max(0, progressPercent));
+ progressBar.style.width = `${progressPercent}%`;
+ if (!useCustomColors)
+ progressBar.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
+ else
+ progressBar.style.setProperty('--accent-color', color1);
+}
\ No newline at end of file
diff --git a/now-playing/themes/album-art/style.css b/now-playing/themes/album-art/style.css
new file mode 100644
index 0000000..06ddf7b
--- /dev/null
+++ b/now-playing/themes/album-art/style.css
@@ -0,0 +1,92 @@
+:root {
+ --border-radius: 30px;
+}
+
+#main-wrapper {
+ width: 500px;
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ gap: 0.5em;
+ opacity: 0;
+ aspect-ratio: 1 / 1;
+}
+
+#album-art-container {
+ aspect-ratio: 1 / 1;
+ overflow: hidden;
+ position: relative;
+ background-size: cover;
+ background-position: center;
+ display: var(--show-album-art);
+}
+
+#album-art-layer,
+#album-art-transition-layer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-size: cover;
+ background-position: center;
+ transition: opacity 0.5s ease;
+ border-radius: var(--border-radius);
+}
+
+#album-art-layer {
+ z-index: 1;
+ transition: all 0.5s ease;
+}
+
+#album-art-transition-layer {
+ z-index: 0;
+}
+
+#song-info-container {
+ position: relative;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ border-radius: var(--border-radius);
+ overflow: hidden;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
+}
+
+#text-info-container {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ padding: 6% 7%;
+ padding-top: 50%;
+ box-sizing: border-box;
+ z-index: 2;
+ text-align: var(--text-alignment);
+}
+
+#track-label {
+ font-size: 2em;
+ font-weight: 700;
+ text-align: var(--text-alignment);
+}
+
+#artist-label {
+ font-size: 1.5em;
+ font-weight: 500;
+ opacity: 0.8;
+ text-align: var(--text-alignment);
+}
+
+#progress-bar {
+ height: 1%;
+ width: 0%; /* JS will control this */
+ background: linear-gradient(to top, #ffffff00, var(--accent-color, #ffffff));
+ /* background-color: var(--accent-color, #ffffff); */
+ transition: width 1s ease-in-out, background 1s ease-in-out;
+ /* border-radius: 10000px; */
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ z-index: 3;
+}
\ No newline at end of file
diff --git a/now-playing/themes/card/index.html b/now-playing/themes/card/index.html
new file mode 100644
index 0000000..7bcf8c2
--- /dev/null
+++ b/now-playing/themes/card/index.html
@@ -0,0 +1,30 @@
+
\ No newline at end of file
diff --git a/now-playing/themes/card/script.js b/now-playing/themes/card/script.js
new file mode 100644
index 0000000..baf4f4c
--- /dev/null
+++ b/now-playing/themes/card/script.js
@@ -0,0 +1,110 @@
+///////////////////
+// PAGE ELEMENTS //
+///////////////////
+
+const mainWrapper = document.getElementById('main-wrapper');
+const albumArtContainer = document.getElementById('album-art-container');
+const songInfoContainer = document.getElementById('song-info-container');
+const albumArtLayer = document.getElementById('album-art-layer');
+const albumArtTransition = document.getElementById('album-art-transition-layer');
+const backgroundLayer = document.getElementById('background-layer');
+const backgroundTransitionLayer = document.getElementById('background-transition-layer');
+const trackLabel = document.getElementById('track-label');
+const artistLabel = document.getElementById('artist-label');
+const progressContainer = document.getElementById('progress-container');
+const progressBarFill = document.getElementById('progress-bar-fill');
+const currentTimeLabel = document.getElementById('current-time');
+const durationLabel = document.getElementById('duration');
+
+
+
+////////////////
+// PAGE SETUP //
+////////////////
+
+// Set property visibility
+trackLabel.style.display = showPrimary ? '' : 'none';
+artistLabel.style.display = showSecondary ? '' : 'none';
+
+// Set container width
+if (maxWidth > 0)
+ mainWrapper.style.width = `${maxWidth}px`;
+else
+ mainWrapper.style.width = `100%`;
+
+// Set progress bar visibility
+if (!showProgressBar)
+ progressContainer.style.display = 'none';
+
+
+
+////////////////////
+// CORE FUNCTIONS //
+////////////////////
+
+async function ChangeTrack(mediaProps, accentColorPalette) {
+ // Fade in the overlay (shows the new text)
+ if (trackLabel.innerText != (swapArtistTrack ? mediaProps.Artist : mediaProps.Title))
+ trackLabel.style.opacity = "0";
+ if (artistLabel.innerText != (swapArtistTrack ? mediaProps.Title : mediaProps.Artist))
+ artistLabel.style.opacity = "0";
+ backgroundLayer.style.opacity = "0";
+ albumArtLayer.style.opacity = "0";
+
+ // Wait for fade (0.5s), then swap the real text and hide overlay
+ setTimeout(() => {
+ SetLabelText('track-label', swapArtistTrack ? mediaProps.Artist : mediaProps.Title);
+ SetLabelText('artist-label', swapArtistTrack ? mediaProps.Title : mediaProps.Artist);
+
+ // Extract the image source string (use fallback if Windows has no art)
+ const newArtUrl = mediaProps.Thumbnail ?? './images/placeholder.png';
+
+ // Set the image
+ backgroundLayer.style.backgroundImage = `url('${newArtUrl}')`;
+ albumArtLayer.style.backgroundImage = `url('${newArtUrl}')`;
+
+ // Apply a tint
+ backgroundLayer.style.backgroundColor = accentColorPalette.DarkMuted + "80"; // 80 is 50% opacity in hex
+
+ trackLabel.style.opacity = "";
+ artistLabel.style.opacity = "";
+ backgroundLayer.style.opacity = "";
+ albumArtLayer.style.opacity = "";
+
+ setTimeout(() => {
+ // Set the image
+ backgroundTransitionLayer.style.backgroundImage = `url('${newArtUrl}')`;
+ albumArtTransition.style.backgroundImage = `url('${newArtUrl}')`;
+
+ // Apply a tint:
+ backgroundTransitionLayer.style.backgroundColor = accentColorPalette.DarkMuted + "80"; // 80 is 50% opacity in hex
+
+ SetVisibility(true); // Show the overlay for a few seconds if autoHide is enabled
+ }, 250);
+ }, 250);
+}
+
+function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
+ // Update the label using your naming convention
+ currentTimeLabel.innerText =
+ ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs);
+
+ durationLabel.innerText =
+ ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime);
+
+ // Set progressbar
+ // Ensure we don't divide by zero or exceed 100%
+ const durationMs = timelineProps.EndTime;
+ let progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0;
+ progressPercent = Math.min(100, Math.max(0, progressPercent));
+ progressBarFill.style.width = `${progressPercent}%`;
+ if (!useCustomColors)
+ {
+ progressBarFill.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
+ }
+ else
+ {
+ progressBarFill.style.setProperty('--accent-color', color1);
+ document.body.style.color = color1;
+ }
+}
\ No newline at end of file
diff --git a/now-playing/themes/card/style.css b/now-playing/themes/card/style.css
new file mode 100644
index 0000000..c578d13
--- /dev/null
+++ b/now-playing/themes/card/style.css
@@ -0,0 +1,172 @@
+:root {
+ --border-radius: 20px;
+ --component-radius: calc(var(--border-radius) * 0.75);
+}
+
+#main-wrapper {
+ width: 500px;
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ gap: 0.5em;
+ opacity: 0;
+}
+
+#album-art-container {
+ aspect-ratio: 1 / 1;
+ overflow: hidden;
+ border-radius: var(--component-radius);
+ position: relative;
+ background-size: cover;
+ background-position: center;
+ margin: 0em 0em 1em 0em;
+ display: var(--show-album-art);
+}
+
+#album-art-layer,
+#album-art-transition-layer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-size: cover;
+ background-position: center;
+ transition: opacity 0.5s ease;
+}
+
+#album-art-layer {
+ z-index: 1;
+ transition: all 0.5s ease;
+}
+
+#album-art-transition-layer {
+ z-index: 0;
+}
+
+#song-info-container {
+ position: relative;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ border-radius: var(--border-radius);
+ overflow: hidden;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
+}
+
+/* The background now fills the frame */
+#background-layer,
+#background-transition-layer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ background-size: cover;
+ background-position: center center;
+ background-repeat: no-repeat;
+
+ background-blend-mode: overlay; /* Or 'soft-light' for a subtler effect */
+ filter: blur(20px) brightness(0.3);
+ transform: scale(1.1); /* Slightly zoom in to cover edges when blurred */
+}
+
+#background-layer {
+ transition: all 0.5s ease;
+}
+
+#background-transition-layer {
+ z-index: 0;
+}
+
+/* The content sits on top */
+#song-info-wrapper {
+ position: relative; /* Keeps it in the document flow */
+ z-index: 2; /* Sits above the background-layer */
+
+ padding: 10% 7.5%;
+ display: flex;
+ flex-direction: column;
+ gap: 0.3em;
+ max-height: 100%;
+}
+
+#track-label {
+ font-size: 1em;
+ font-weight: 600;
+ text-align: var(--text-alignment);
+}
+
+#artist-label {
+ font-size: 0.8em;
+ font-weight: 300;
+ opacity: 0.6;
+ text-align: var(--text-alignment);
+}
+
+.text-label {
+ white-space: nowrap;
+ /* overflow: hidden; */
+ mask-image: var(--trailing-fade);
+ -webkit-mask-image: var(--trailing-fade);
+ transition: opacity 0.25s ease-in-out;
+}
+
+#progress-container {
+ display: flex;
+ flex-direction: column;
+ gap: 0.2em;
+ margin-top: 1em;
+}
+
+#progress-bar-track {
+ width: 100%;
+ height: 0.4em;
+ border-radius: 10000px;
+ background-color: #ffffff3a;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); /* Adds depth */
+}
+
+#progress-bar-fill {
+ height: 100%;
+ width: 0%; /* JS will control this */
+ border-radius: 10000px;
+ background-color: var(--accent-color, #ffffff);
+ transition: width 1s ease-in-out, background-color 1s ease-in-out;
+
+ /* This makes the handle track the end of the bar */
+ position: relative;
+}
+
+#progress-bar-fill::after {
+ content: '';
+ position: absolute;
+ right: -6px; /* Adjust to center the circle on the end of the bar */
+ top: 50%;
+ transform: translateY(-50%);
+
+ width: 0.8em; /* Diameter of your circle */
+ height: 0.8em;
+ background-color: var(--accent-color, #ffffff);
+ border-radius: 50%;
+ filter: brightness(1.4);
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); /* Adds depth */
+
+ /* Optional: Hide it if the width is too small */
+ opacity: 0;
+ transition: all 1s ease-in-out;
+}
+
+/* Show the handle when the progress bar is active */
+#progress-bar-track:hover #progress-bar-fill::after,
+#progress-bar-fill:not([style*="width: 0%"])::after {
+ opacity: 1;
+}
+
+#time-container {
+ display: flex;
+ justify-content: space-between;
+ font-size: 0.7em;
+ opacity: 0.5;
+}
\ No newline at end of file
diff --git a/now-playing/themes/classic/index.html b/now-playing/themes/classic/index.html
new file mode 100644
index 0000000..5aab0e6
--- /dev/null
+++ b/now-playing/themes/classic/index.html
@@ -0,0 +1,30 @@
+
\ No newline at end of file
diff --git a/now-playing/themes/classic/script.js b/now-playing/themes/classic/script.js
new file mode 100644
index 0000000..64b7fd6
--- /dev/null
+++ b/now-playing/themes/classic/script.js
@@ -0,0 +1,110 @@
+///////////////////
+// PAGE ELEMENTS //
+///////////////////
+
+const mainWrapper = document.getElementById('main-wrapper');
+const albumArtContainer = document.getElementById('album-art-container');
+const songInfoContainer = document.getElementById('song-info-container');
+const albumArtLayer = document.getElementById('album-art-layer');
+const albumArtTransition = document.getElementById('album-art-transition-layer');
+const backgroundLayer = document.getElementById('background-layer');
+const backgroundTransitionLayer = document.getElementById('background-transition-layer');
+const trackLabel = document.getElementById('track-label');
+const artistLabel = document.getElementById('artist-label');
+const progressContainer = document.getElementById('progress-container');
+const progressBarFill = document.getElementById('progress-bar-fill');
+const currentTimeLabel = document.getElementById('current-time');
+const durationLabel = document.getElementById('duration');
+
+
+
+////////////////
+// PAGE SETUP //
+////////////////
+
+// Set property visibility
+trackLabel.style.display = showPrimary ? '' : 'none';
+artistLabel.style.display = showSecondary ? '' : 'none';
+
+// Set container width
+if (maxWidth > 0)
+ mainWrapper.style.width = `${maxWidth}px`;
+else
+ mainWrapper.style.width = `100%`;
+
+// Set progress bar visibility
+if (!showProgressBar)
+ progressContainer.style.display = 'none';
+
+
+
+////////////////////
+// CORE FUNCTIONS //
+////////////////////
+
+async function ChangeTrack(mediaProps, accentColorPalette) {
+ // Fade in the overlay (shows the new text)
+ if (trackLabel.innerText != (swapArtistTrack ? mediaProps.Artist : mediaProps.Title))
+ trackLabel.style.opacity = "0";
+ if (artistLabel.innerText != (swapArtistTrack ? mediaProps.Title : mediaProps.Artist))
+ artistLabel.style.opacity = "0";
+ backgroundLayer.style.opacity = "0";
+ albumArtLayer.style.opacity = "0";
+
+ // Wait for fade (0.5s), then swap the real text and hide overlay
+ setTimeout(async () => {
+ SetLabelText('track-label', swapArtistTrack ? mediaProps.Artist : mediaProps.Title);
+ SetLabelText('artist-label', swapArtistTrack ? mediaProps.Title : mediaProps.Artist);
+
+ // Extract the image source string (use fallback if Windows has no art)
+ const newArtUrl = mediaProps.Thumbnail ?? './images/placeholder.png';
+
+ // Set the image
+ backgroundLayer.style.backgroundImage = `url('${newArtUrl}')`;
+ albumArtLayer.style.backgroundImage = `url('${newArtUrl}')`;
+
+ // Apply a tint
+ backgroundLayer.style.backgroundColor = accentColorPalette.DarkMuted + "80"; // 80 is 50% opacity in hex
+
+ trackLabel.style.opacity = "";
+ artistLabel.style.opacity = "";
+ backgroundLayer.style.opacity = "";
+ albumArtLayer.style.opacity = "";
+
+ setTimeout(() => {
+ // Set the image
+ backgroundTransitionLayer.style.backgroundImage = `url('${newArtUrl}')`;
+ albumArtTransition.style.backgroundImage = `url('${newArtUrl}')`;
+
+ // Apply a tint
+ backgroundTransitionLayer.style.backgroundColor = accentColorPalette.DarkMuted + "80"; // 80 is 50% opacity in hex
+
+ SetVisibility(true); // Show the overlay for a few seconds if autoHide is enabled
+ }, 250);
+ }, 250);
+}
+
+function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
+ // Update the label using your naming convention
+ currentTimeLabel.innerText =
+ ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs);
+
+ durationLabel.innerText =
+ ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime);
+
+ // Set progressbar
+ // Ensure we don't divide by zero or exceed 100%
+ const durationMs = timelineProps.EndTime;
+ let progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0;
+ progressPercent = Math.min(100, Math.max(0, progressPercent));
+ progressBarFill.style.width = `${progressPercent}%`;
+ if (!useCustomColors)
+ {
+ progressBarFill.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
+ }
+ else
+ {
+ progressBarFill.style.setProperty('--accent-color', color1);
+ document.body.style.color = color1;
+ }
+}
\ No newline at end of file
diff --git a/now-playing/themes/classic/style.css b/now-playing/themes/classic/style.css
new file mode 100644
index 0000000..1223703
--- /dev/null
+++ b/now-playing/themes/classic/style.css
@@ -0,0 +1,185 @@
+:root {
+ --border-radius: 20px;
+}
+
+#main-wrapper {
+ width: 500px;
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ gap: 0.5em;
+ opacity: 0;
+}
+
+#album-art-container {
+ /* 1. Tell it to maintain a 1:1 square ratio */
+ height: 100%;
+ aspect-ratio: 1 / 1;
+ overflow: hidden;
+ border-radius: var(--border-radius);
+ position: relative;
+ background-size: cover;
+ background-position: center;
+ display: var(--show-album-art);
+ flex-shrink: 0;
+}
+
+#album-art-layer,
+#album-art-transition-layer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-size: cover;
+ background-position: center;
+ transition: opacity 0.5s ease;
+}
+
+#album-art-layer {
+ z-index: 1;
+ transition: all 0.5s ease;
+}
+
+#album-art-transition-layer {
+ z-index: 0;
+}
+
+#song-info-container {
+ position: relative;
+ flex-grow: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ border-radius: var(--border-radius);
+ overflow: hidden;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
+}
+
+#yet-another-wrapper-yup {
+ position: relative;
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ overflow: hidden;
+ gap: 1em;
+ transition: all 0.25s ease-in-out;
+}
+
+/* The background now fills the frame */
+#background-layer,
+#background-transition-layer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ background-size: cover;
+ background-position: center center;
+ background-repeat: no-repeat;
+
+ background-blend-mode: overlay; /* Or 'soft-light' for a subtler effect */
+ filter: blur(10px) brightness(0.3) saturate(1.2);
+ transform: scale(1.1); /* Slightly zoom in to cover edges when blurred */
+}
+
+#background-layer {
+ transition: all 0.5s ease;
+}
+
+#background-transition-layer {
+ z-index: 0;
+}
+
+/* The content sits on top */
+#song-info-wrapper {
+ position: relative; /* Keeps it in the document flow */
+ z-index: 2; /* Sits above the background-layer */
+
+ padding: 1em 1.3em;
+ display: flex;
+ flex-direction: column;
+ gap: 0.3em;
+}
+
+#track-label {
+ font-size: 1em;
+ font-weight: 600;
+ text-align: var(--text-alignment);
+}
+
+#artist-label {
+ font-size: 0.8em;
+ font-weight: 300;
+ opacity: 0.6;
+ text-align: var(--text-alignment);
+}
+
+.text-label {
+ white-space: nowrap;
+ overflow: hidden;
+ mask-image: var(--trailing-fade);
+ -webkit-mask-image: var(--trailing-fade);
+ transition: opacity 0.25s ease-in-out;
+}
+
+#progress-container {
+ display: flex;
+ flex-direction: column;
+ gap: 0.2em;
+ margin-top: 0.2em;
+}
+
+#progress-bar-track {
+ width: 100%;
+ height: 0.4em;
+ border-radius: 10000px;
+ background-color: #ffffff3a;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); /* Adds depth */
+}
+
+#progress-bar-fill {
+ height: 100%;
+ width: 0%; /* JS will control this */
+ border-radius: 10000px;
+ background-color: var(--accent-color, #ffffff);
+ transition: width 1s ease-in-out, background-color 1s ease-in-out;
+
+ /* This makes the handle track the end of the bar */
+ position: relative;
+}
+
+#progress-bar-fill::after {
+ content: '';
+ position: absolute;
+ right: -6px; /* Adjust to center the circle on the end of the bar */
+ top: 50%;
+ transform: translateY(-50%);
+
+ width: 0.8em; /* Diameter of your circle */
+ height: 0.8em;
+ background-color: var(--accent-color, #ffffff);
+ border-radius: 50%;
+ filter: brightness(1.4);
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); /* Adds depth */
+
+ /* Optional: Hide it if the width is too small */
+ opacity: 0;
+ transition: all 1s ease-in-out;
+}
+
+/* Show the handle when the progress bar is active */
+#progress-bar-track:hover #progress-bar-fill::after,
+#progress-bar-fill:not([style*="width: 0%"])::after {
+ opacity: 1;
+}
+
+#time-container {
+ display: flex;
+ justify-content: space-between;
+ font-size: 0.7em;
+ opacity: 0.5;
+}
\ No newline at end of file
diff --git a/now-playing/themes/color-palette/index.html b/now-playing/themes/color-palette/index.html
new file mode 100644
index 0000000..c5e07ba
--- /dev/null
+++ b/now-playing/themes/color-palette/index.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/now-playing/themes/color-palette/script.js b/now-playing/themes/color-palette/script.js
new file mode 100644
index 0000000..bffb9b2
--- /dev/null
+++ b/now-playing/themes/color-palette/script.js
@@ -0,0 +1,125 @@
+///////////////////
+// PAGE ELEMENTS //
+///////////////////
+
+const mainWrapper = document.getElementById('main-wrapper');
+// Color Boxes
+const boxVibrant = document.getElementById('box-vibrant');
+const boxMuted = document.getElementById('box-muted');
+const boxDarkVibrant = document.getElementById('box-dark-vibrant');
+const boxDarkMuted = document.getElementById('box-dark-muted');
+const boxLightVibrant = document.getElementById('box-light-vibrant');
+const boxLightMuted = document.getElementById('box-light-muted');
+
+// Primary Text Labels (for Hex codes)
+const labelVibrant = document.getElementById('label-vibrant');
+const labelMuted = document.getElementById('label-muted');
+const labelDarkVibrant = document.getElementById('label-dark-vibrant');
+const labelDarkMuted = document.getElementById('label-dark-muted');
+const labelLightVibrant = document.getElementById('label-light-vibrant');
+const labelLightMuted = document.getElementById('label-light-muted');
+
+
+
+/////////////////
+// GLOBAL VARS //
+/////////////////
+
+const colorRoles = ['Vibrant', 'Muted', 'DarkVibrant', 'DarkMuted', 'LightVibrant', 'LightMuted'];
+
+
+
+////////////////
+// PAGE SETUP //
+////////////////
+
+// Set container width
+if (maxWidth > 0)
+ mainWrapper.style.width = `${maxWidth}px`;
+else
+ mainWrapper.style.width = `100%`;
+
+// Set primary and secondary text visibility
+if (showPrimary)
+ document.documentElement.style.setProperty('--show-primary', '');
+else
+ document.documentElement.style.setProperty('--show-primary', 'none');
+
+if (showSecondary)
+ document.documentElement.style.setProperty('--show-secondary', '');
+else
+ document.documentElement.style.setProperty('--show-secondary', 'none');
+
+
+
+////////////////////
+// CORE FUNCTIONS //
+////////////////////
+
+async function ChangeTrack(mediaProps, accentColorPalette) {
+ // Map elements for easy lookup
+ const labels = {
+ Vibrant: labelVibrant,
+ Muted: labelMuted,
+ DarkVibrant: labelDarkVibrant,
+ DarkMuted: labelDarkMuted,
+ LightVibrant: labelLightVibrant,
+ LightMuted: labelLightMuted
+ };
+
+ const boxes = {
+ Vibrant: boxVibrant,
+ Muted: boxMuted,
+ DarkVibrant: boxDarkVibrant,
+ DarkMuted: boxDarkMuted,
+ LightVibrant: boxLightVibrant,
+ LightMuted: boxLightMuted
+ };
+
+ // Fade out all labels
+ colorRoles.forEach(role => {
+ labels[role].style.opacity = 0;
+ });
+
+ // Wait for fade, then swap text, colors, and restore opacity
+ setTimeout(() => {
+ colorRoles.forEach(role => {
+ const colorValue = accentColorPalette[role];
+
+ labels[role].textContent = colorValue;
+ boxes[role].style.setProperty('--box-bg', colorValue);
+ boxes[role].style.color = colorValue;
+ labels[role].style.opacity = '';
+ });
+ }, 250);
+}
+
+function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
+ const durationMs = timelineProps.EndTime;
+
+ let progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0;
+ progressPercent = Math.min(100, Math.max(0, progressPercent));
+
+ if (durationMs <= 0 || !showProgressBar)
+ progressPercent = 100;
+
+ const clipRight = 100 - progressPercent;
+
+ const boxes = {
+ Vibrant: boxVibrant,
+ Muted: boxMuted,
+ DarkVibrant: boxDarkVibrant,
+ DarkMuted: boxDarkMuted,
+ LightVibrant: boxLightVibrant,
+ LightMuted: boxLightMuted
+ };
+
+ // Update variables for each box dynamically via loop
+ colorRoles.forEach(role => {
+ const box = boxes[role];
+ if (box && accentColorPalette[role]) {
+ box.style.setProperty('--box-bg', accentColorPalette[role]);
+ box.style.setProperty('--clip-right', `${clipRight}%`);
+ }
+ });
+}
\ No newline at end of file
diff --git a/now-playing/themes/color-palette/style.css b/now-playing/themes/color-palette/style.css
new file mode 100644
index 0000000..e4a3090
--- /dev/null
+++ b/now-playing/themes/color-palette/style.css
@@ -0,0 +1,64 @@
+:root {
+ --border-radius: 10px;
+}
+
+#main-wrapper {
+ width: 500px;
+ opacity: 0;
+}
+
+#color-palette {
+ display: flex;
+ gap: 0.5em;
+ flex-direction: column;
+ width: 100%;
+}
+
+.color-box {
+ display: flex;
+ align-items: flex-end;
+ padding: 0.5em;
+ border-radius: var(--border-radius);
+ text-align: var(--text-alignment);
+ height: 5em;
+ position: relative;
+ overflow: hidden;
+}
+
+.color-box::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: var(--box-bg);
+ clip-path: inset(0 var(--clip-right, 0%) 0 0);
+ z-index: 1;
+ transition: all 1s ease;
+}
+
+.color-box .color-info {
+ position: relative;
+ z-index: 2;
+}
+
+.color-info {
+ text-transform: uppercase;
+ opacity: 0.5;
+ width: 100%;
+ mix-blend-mode: difference;
+ display: flex;
+ flex-direction: column;
+}
+
+.primary-text {
+ font-size: 1em;
+ font-weight: 700;
+ transition: opacity 0.5s ease;
+ display: var(--show-primary);
+}
+
+.secondary-text {
+ opacity: 0.7;
+ font-size: 0.7em;
+ font-weight: 700;
+ display: var(--show-secondary);
+}
\ No newline at end of file
diff --git a/now-playing/themes/compact-inverted/inherits.json b/now-playing/themes/compact-inverted/inherits.json
new file mode 100644
index 0000000..9bd2662
--- /dev/null
+++ b/now-playing/themes/compact-inverted/inherits.json
@@ -0,0 +1,3 @@
+{
+ "base-theme": "compact"
+}
\ No newline at end of file
diff --git a/now-playing/themes/compact-inverted/style.css b/now-playing/themes/compact-inverted/style.css
new file mode 100644
index 0000000..e69de29
diff --git a/now-playing/themes/compact/index.html b/now-playing/themes/compact/index.html
new file mode 100644
index 0000000..3da3f5f
--- /dev/null
+++ b/now-playing/themes/compact/index.html
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/now-playing/themes/compact/script.js b/now-playing/themes/compact/script.js
new file mode 100644
index 0000000..a8b736d
--- /dev/null
+++ b/now-playing/themes/compact/script.js
@@ -0,0 +1,193 @@
+///////////////////
+// PAGE ELEMENTS //
+///////////////////
+
+const mainWrapper = document.getElementById('main-wrapper');
+const albumArtContainer = document.getElementById('album-art-container');
+const songInfoContainer = document.getElementById('song-info-container');
+const progressBar = document.getElementById('progress-bar');
+const progressBarTrack = document.getElementById('progress-bar-track');
+const songLabel = document.getElementById('song-label');
+const songLabelBackground = document.getElementById('song-label-background');
+
+
+
+////////////////
+// PAGE SETUP //
+////////////////
+
+// This theme has variants
+const themeVariant = window.ThemeVariant ? window.ThemeVariant : '';
+
+// Set property visibility
+if (!showPrimary)
+ document.documentElement.style.setProperty('--show-primary', `none`);
+if (!showSecondary)
+ document.documentElement.style.setProperty('--show-secondary', `none`);
+
+// Set container width
+if (maxWidth > 0)
+ mainWrapper.style.width = `${maxWidth}px`;
+else
+ mainWrapper.style.width = `100%`;
+
+
+
+////////////////////
+// CORE FUNCTIONS //
+////////////////////
+
+async function ChangeTrack(mediaProps, accentColorPalette) {
+ // Fade in the overlay (shows the new text)
+ songLabel.style.opacity = "0";
+ songLabelBackground.style.opacity = "0";
+
+ // Wait for fade (0.5s), then swap the real text and hide overlay
+ setTimeout(() => {
+ if (swapArtistTrack) {
+ SetSongInfo(mediaProps.Artist, mediaProps.Title);
+ } else {
+ SetSongInfo(mediaProps.Title, mediaProps.Artist);
+ }
+
+ // Extract the image source string (use fallback if Windows has no art)
+
+ // Set the pill color
+ if (!useCustomColors)
+ {
+ switch (themeVariant) {
+ case "compact-inverted":
+ progressBarTrack.style.backgroundColor = accentColorPalette.LightVibrant;
+ progressBar.style.background = `color-mix(in srgb, ${accentColorPalette.DarkMuted}, black 60%)`;
+ songLabel.style.color = accentColorPalette.LightVibrant;
+ songLabelBackground.style.color = accentColorPalette.DarkVibrant;
+ break;
+ default:
+ progressBarTrack.style.backgroundColor = `color-mix(in srgb, ${accentColorPalette.DarkMuted}, black 60%)`;
+ progressBar.style.background = accentColorPalette.LightVibrant;
+ songLabel.style.color = accentColorPalette.DarkVibrant;
+ songLabelBackground.style.color = accentColorPalette.LightVibrant;
+ break;
+ }
+ }
+ else
+ {
+ switch (themeVariant) {
+ case "compact-inverted":
+ progressBarTrack.style.backgroundColor = `color-mix(in srgb, ${color1}, black 60%)`;
+ progressBar.style.background = color2;
+ songLabel.style.color = color1;
+ songLabelBackground.style.color = color2;
+ break;
+ default:
+ progressBarTrack.style.backgroundColor = color2;
+ progressBar.style.background = color1;
+ songLabel.style.color = color2;
+ songLabelBackground.style.color = color1;
+ break;
+ }
+ }
+
+ songLabel.style.opacity = "";
+ songLabelBackground.style.opacity = "";
+
+ setTimeout(() => {
+ SetVisibility(true); // Show the overlay for a few seconds if autoHide is enabled
+ }, 250);
+ }, 250);
+}
+
+function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
+ // Set progressbar
+ // Ensure we don't divide by zero or exceed 100%
+ const durationMs = timelineProps.EndTime;
+
+ // If the duration is 0, that means the session isn't returning any timeline info, so just put the progress at 100%
+ if (durationMs <= 0 || !showProgressBar)
+ {
+ progressBarTrack.style.display = 'none';
+ return;
+ }
+ else
+ progressBarTrack.style.display = '';
+
+ // Calculate the progress percentage
+ let progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0;
+ progressPercent = Math.min(100, Math.max(0, progressPercent));
+
+ // Calculate how much needs to be hidden (the right-side offset)
+ const clipRight = 100 - progressPercent;
+
+ // Update the clip-path
+ progressBar.style.clipPath = `inset(0 ${clipRight}% 0 0)`;
+ progressBarTrack.style.clipPath = `inset(0 0 0 ${progressPercent}%)`;
+
+ // // Set the progress bar but with a transition for smoothness
+ // const clipRight = progressPercent;
+ // const transitionWidth = '1em';
+ // progressBar.style.maskImage = `linear-gradient(to right, black calc(${clipRight}% - ${transitionWidth}), transparent ${clipRight}%)`
+ // progressBarTrack.style.maskImage = `linear-gradient(to right, transparent calc(${clipRight}% - ${transitionWidth}), black ${clipRight}%)`;
+}
+
+// MARQUEE LOGIC
+// This is a more advanced marquee implementation that calculates the overflow distance and adjusts the scroll speed accordingly
+
+// Call this function whenever the track changes
+function SetSongInfo(trackName, artistName) {
+ // Update main text and background layer simultaneously
+ UpdateSingleSongLabel('song-label', trackName, artistName);
+ UpdateSingleSongLabel('song-label-background', trackName, artistName);
+}
+
+function UpdateSingleSongLabel(containerId, trackName, artistName) {
+ const container = document.getElementById(containerId);
+ if (!container) return;
+
+ let scrollContent = container.querySelector('.scroll-content');
+
+ // Only rebuild the HTML structure if the text content actually changed or doesn't exist yet
+ const currentTrackEl = container.querySelector('.track-label');
+ const currentArtistEl = container.querySelector('.artist-label');
+
+ if (!scrollContent || !currentTrackEl || currentTrackEl.textContent !== trackName || currentArtistEl.textContent !== artistName) {
+ container.classList.remove('is-overflowing');
+ container.innerHTML = `
+
+ ${trackName}
+ ${artistName}
+
+ `;
+ scrollContent = container.querySelector('.scroll-content');
+ }
+
+ // Encapsulate measurement logic so it can be called safely by the observer
+ const updateMetrics = () => {
+ scrollContent = container.querySelector('.scroll-content');
+
+ const textWidth = scrollContent.scrollWidth;
+ const containerWidth = container.clientWidth;
+ const overflowDistance = textWidth - containerWidth;
+
+ if (overflowDistance > 0) {
+ const distancePercent = (overflowDistance / textWidth) * 100;
+ const travelTime = overflowDistance / SCROLL_SPEED_PX_PER_SEC;
+ const totalDuration = (travelTime * 2) + 3;
+
+ container.style.setProperty('--scroll-distance', `-${distancePercent}%`);
+ container.style.setProperty('--marquee-duration', `${totalDuration}s`);
+ container.classList.add('is-overflowing');
+ } else {
+ container.classList.remove('is-overflowing');
+ }
+ };
+
+ updateMetrics();
+
+ // Attach ResizeObserver once to update metrics only (No recursive function resets!)
+ if (!container._resizeObserver) {
+ container._resizeObserver = new ResizeObserver(() => {
+ updateMetrics();
+ });
+ container._resizeObserver.observe(container);
+ }
+}
\ No newline at end of file
diff --git a/now-playing/themes/compact/style.css b/now-playing/themes/compact/style.css
new file mode 100644
index 0000000..9b87c0c
--- /dev/null
+++ b/now-playing/themes/compact/style.css
@@ -0,0 +1,102 @@
+#main-wrapper {
+ width: 500px;
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ gap: 1em;
+ opacity: 0;
+ align-items: center;
+}
+
+#song-info-container {
+ position: relative;
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+ overflow: hidden;
+ align-items: center;
+ border-radius: 1em;
+ transition: all 1s ease-in-out;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); /* Adds depth */
+
+ display: grid;
+ /* This creates a single cell that both children will occupy */
+ grid-template-areas: "stack";
+}
+
+/* The content sits on top */
+#progress-bar {
+ position: relative;
+ z-index: 2; /* Sits above the progress bar */
+ transition: all 1s ease-in-out;
+ width: 100%;
+}
+
+#progress-bar-track {
+ transition: all 1s ease-in-out;
+}
+
+.progress {
+ box-sizing: border-box;
+ padding: 0.25em 0.75em;
+ grid-area: stack;
+ display: flex;
+ min-width: 0;
+}
+
+#song-label,
+#song-label-background {
+ width: 100%;
+ text-align: var(--text-alignment);
+}
+
+#artist-label,
+#artist-label-background {
+ font-size: 0.8em;
+ font-weight: 400;
+ opacity: 0.7;
+}
+
+#track-label {
+ font-weight: 700;
+}
+
+.text-label {
+ transition: opacity 0.25s ease-in-out;
+}
+
+/* Inner wrapper holding track + artist inline */
+#song-label .scroll-content,
+#song-label-background .scroll-content {
+ display: inline-flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 0.5em; /* Spacing between Track and Artist */
+ white-space: nowrap;
+ will-change: transform;
+}
+
+/* Preserve original font weights */
+.track-label {
+ font-weight: 700;
+ display: var(--show-primary);
+}
+
+.artist-label {
+ font-size: 0.8em;
+ font-weight: 400;
+ opacity: 0.7;
+ display: var(--show-secondary);
+}
+
+/* Marquee scroll animation */
+#song-label.is-overflowing .scroll-content,
+#song-label-background.is-overflowing .scroll-content {
+ animation: ping-pong-scroll var(--marquee-duration, 8s) ease-in-out infinite;
+}
+
+/* Mask animation applied to container */
+#song-label.is-overflowing,
+#song-label-background.is-overflowing {
+ animation: ping-pong-mask var(--marquee-duration, 8s) ease-in-out infinite;
+}
\ No newline at end of file
diff --git a/now-playing/themes/matte-dark/inherits.json b/now-playing/themes/matte-dark/inherits.json
new file mode 100644
index 0000000..19733b4
--- /dev/null
+++ b/now-playing/themes/matte-dark/inherits.json
@@ -0,0 +1,3 @@
+{
+ "base-theme": "standard"
+}
\ No newline at end of file
diff --git a/now-playing/themes/matte-dark/style.css b/now-playing/themes/matte-dark/style.css
new file mode 100644
index 0000000..bef8978
--- /dev/null
+++ b/now-playing/themes/matte-dark/style.css
@@ -0,0 +1,17 @@
+#track-label {
+ font-weight: 800;
+}
+
+#artist-label {
+ font-weight: 500;
+ opacity: 0.7;
+}
+
+.progress-text {
+ font-weight: 500;
+ opacity: 0.7;
+}
+
+#progress-bar-track {
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); /* Adds depth */
+}
\ No newline at end of file
diff --git a/now-playing/themes/matte/inherits.json b/now-playing/themes/matte/inherits.json
new file mode 100644
index 0000000..19733b4
--- /dev/null
+++ b/now-playing/themes/matte/inherits.json
@@ -0,0 +1,3 @@
+{
+ "base-theme": "standard"
+}
\ No newline at end of file
diff --git a/now-playing/themes/matte/style.css b/now-playing/themes/matte/style.css
new file mode 100644
index 0000000..bef8978
--- /dev/null
+++ b/now-playing/themes/matte/style.css
@@ -0,0 +1,17 @@
+#track-label {
+ font-weight: 800;
+}
+
+#artist-label {
+ font-weight: 500;
+ opacity: 0.7;
+}
+
+.progress-text {
+ font-weight: 500;
+ opacity: 0.7;
+}
+
+#progress-bar-track {
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); /* Adds depth */
+}
\ No newline at end of file
diff --git a/now-playing/themes/simple/index.html b/now-playing/themes/simple/index.html
new file mode 100644
index 0000000..5c200da
--- /dev/null
+++ b/now-playing/themes/simple/index.html
@@ -0,0 +1,19 @@
+
\ No newline at end of file
diff --git a/now-playing/themes/simple/script.js b/now-playing/themes/simple/script.js
new file mode 100644
index 0000000..5b85975
--- /dev/null
+++ b/now-playing/themes/simple/script.js
@@ -0,0 +1,127 @@
+///////////////////
+// PAGE ELEMENTS //
+///////////////////
+
+const mainWrapper = document.getElementById('main-wrapper');
+const albumArtContainer = document.getElementById('album-art-container');
+const songInfoContainer = document.getElementById('song-info-container');
+const albumArtLayer = document.getElementById('album-art-layer');
+const albumArtTransition = document.getElementById('album-art-transition-layer');
+const trackLabel = document.getElementById('track-label');
+const artistLabel = document.getElementById('artist-label');
+const progressContainer = document.getElementById('progress-container');
+const progressBarFill = document.getElementById('progress-bar-fill');
+const currentTimeLabel = document.getElementById('current-time');
+const durationLabel = document.getElementById('duration');
+
+
+
+////////////////
+// PAGE SETUP //
+////////////////
+
+// Set property visibility
+trackLabel.style.display = showPrimary ? '' : 'none';
+artistLabel.style.display = showSecondary ? '' : 'none';
+
+// Set container width
+if (maxWidth > 0)
+ mainWrapper.style.width = `${maxWidth}px`;
+else
+ mainWrapper.style.width = `100%`;
+
+// Set progress bar visibility
+if (!showProgressBar)
+ progressContainer.style.display = 'none';
+
+// If text alignment is 'right', swap the album art to the right hand side too
+if (textAlignment == 'right') {
+ mainWrapper.style.flexDirection = 'row-reverse';
+}
+
+
+
+////////////////////
+// CORE FUNCTIONS //
+////////////////////
+
+async function ChangeTrack(mediaProps) {
+ if (trackLabel.innerText != (swapArtistTrack ? mediaProps.Artist : mediaProps.Title))
+ trackLabel.style.opacity = "0";
+ if (artistLabel.innerText != (swapArtistTrack ? mediaProps.Title : mediaProps.Artist))
+ artistLabel.style.opacity = "0";
+ albumArtLayer.style.opacity = "0";
+
+ // Wait for fade (0.5s), then swap the real text and hide overlay
+ setTimeout(() => {
+ // trackLabel.innerText = swapArtistTrack ? mediaProps.Artist : mediaProps.Title;
+ // artistLabel.innerText = swapArtistTrack ? mediaProps.Title : mediaProps.Artist;
+ SetLabelText('track-label', swapArtistTrack ? mediaProps.Artist : mediaProps.Title);
+ SetLabelText('artist-label', swapArtistTrack ? mediaProps.Title : mediaProps.Artist);
+
+ // Extract the image source string (use fallback if Windows has no art)
+ const newArtUrl = mediaProps.Thumbnail ?? './images/placeholder.png';
+
+ // Set the image
+ albumArtLayer.style.backgroundImage = `url('${newArtUrl}')`;
+
+ trackLabel.style.opacity = "";
+ artistLabel.style.opacity = "";
+ albumArtLayer.style.opacity = "";
+
+ setTimeout(() => {
+ // Set the image
+ albumArtTransition.style.backgroundImage = `url('${newArtUrl}')`;
+
+ SetVisibility(true); // Show the overlay for a few seconds if autoHide is enabled
+ }, 250);
+ }, 250);
+}
+
+function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
+ // Update the label using your naming convention
+ currentTimeLabel.innerText =
+ ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs);
+
+ durationLabel.innerText =
+ ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime);
+
+ // Set progressbar
+ // Ensure we don't divide by zero or exceed 100%
+ const durationMs = timelineProps.EndTime;
+ let progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0;
+ progressPercent = Math.min(100, Math.max(0, progressPercent));
+ progressBarFill.style.width = `${progressPercent}%`;
+
+ if (!useCustomColors)
+ {
+ progressBarFill.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
+ document.body.style.color = accentColorPalette.LightVibrant;
+ }
+ else
+ {
+ progressBarFill.style.setProperty('--accent-color', color1);
+ document.body.style.color = color1;
+ }
+}
+
+
+// Helper function to calculate height of album art
+const observer = new ResizeObserver(entries => {
+ for (let entry of entries) {
+ // Get the accurate rendered height of song-info-container
+ const infoHeight = entry.contentRect.height;
+
+ // Calculate 125% of that height
+ const targetSize = infoHeight * 1.5;
+
+ console.log(targetSize);
+
+ // Apply to album art (setting both width & height ensures it stays square)
+ albumArtContainer.style.height = `${targetSize}px`;
+ albumArtContainer.style.width = `${targetSize}px`;
+ }
+});
+
+// Start watching the song info container for size changes
+observer.observe(songInfoContainer);
\ No newline at end of file
diff --git a/now-playing/themes/simple/style.css b/now-playing/themes/simple/style.css
new file mode 100644
index 0000000..283f1d7
--- /dev/null
+++ b/now-playing/themes/simple/style.css
@@ -0,0 +1,115 @@
+:root {
+ --border-radius: 10px;
+}
+
+#main-wrapper {
+ width: 500px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 1em;
+ opacity: 0;
+}
+
+#album-art-container {
+ /* 1. Tell it to maintain a 1:1 square ratio */
+ aspect-ratio: 1 / 1;
+ overflow: hidden;
+ border-radius: var(--border-radius);
+ position: relative;
+ background-size: cover;
+ background-position: center;
+ display: var(--show-album-art);
+ flex-shrink: 0;
+}
+
+#album-art-layer,
+#album-art-transition-layer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-size: cover;
+ background-position: center;
+ transition: opacity 0.5s ease;
+}
+
+#album-art-layer {
+ z-index: 1;
+ transition: all 0.5s ease;
+}
+
+#album-art-transition-layer {
+ z-index: 0;
+}
+
+#song-info-container {
+ position: relative;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ flex-grow: 1;
+ min-width: 0;
+ gap: 0.25em;
+ transition: color 1s ease;
+}
+
+#background-transition-layer {
+ z-index: 0;
+}
+
+#track-label {
+ font-size: 1em;
+ font-weight: 600;
+ text-align: var(--text-alignment);
+}
+
+#artist-label {
+ font-size: 0.8em;
+ font-weight: 400;
+ opacity: 0.6;
+ text-align: var(--text-alignment);
+}
+
+#progress-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 0.2em;
+ margin-top: 0.2em;
+ gap: 0.5em;
+}
+
+#progress-bar-track {
+ width: 100%;
+ height: 0.4em;
+ border-radius: 1em;
+ background-color: #ffffff3a;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); /* Adds depth */
+}
+
+#progress-bar-fill {
+ height: 100%;
+ width: 0%; /* JS will control this */
+ border-radius: 1em;
+ background-color: var(--accent-color, #ffffff);
+ transition: width 1s ease-in-out, background-color 1s ease-in-out;
+
+ /* This makes the handle track the end of the bar */
+ position: relative;
+}
+
+.progress-text {
+ font-size: 0.7em;
+ opacity: 0.5;
+}
+
+.text-label {
+ white-space: nowrap;
+ overflow: hidden;
+ mask-image: var(--trailing-fade);
+ -webkit-mask-image: var(--trailing-fade);
+ transition: opacity 0.25s ease-in-out;
+}
\ No newline at end of file
diff --git a/now-playing/themes/standard/index.html b/now-playing/themes/standard/index.html
new file mode 100644
index 0000000..bd9b89c
--- /dev/null
+++ b/now-playing/themes/standard/index.html
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/now-playing/themes/standard/script.js b/now-playing/themes/standard/script.js
new file mode 100644
index 0000000..0ca049a
--- /dev/null
+++ b/now-playing/themes/standard/script.js
@@ -0,0 +1,188 @@
+///////////////////
+// PAGE ELEMENTS //
+///////////////////
+
+const mainWrapper = document.getElementById('main-wrapper');
+const albumArtContainer = document.getElementById('album-art-container');
+const songInfoContainer = document.getElementById('song-info-container');
+const songInfoWrapper = document.getElementById('song-info-wrapper');
+const albumArtLayer = document.getElementById('album-art-layer');
+const albumArtTransition = document.getElementById('album-art-transition-layer');
+const backgroundLayer = document.getElementById('background-layer');
+const backgroundTransitionLayer = document.getElementById('background-transition-layer');
+const trackLabel = document.getElementById('track-label');
+const artistLabel = document.getElementById('artist-label');
+const progressContainer = document.getElementById('progress-container');
+const progressBarFill = document.getElementById('progress-bar-fill');
+const currentTimeLabel = document.getElementById('current-time');
+const durationLabel = document.getElementById('duration');
+
+
+
+////////////////
+// PAGE SETUP //
+////////////////
+
+// This theme has variants
+const themeVariant = window.ThemeVariant ? window.ThemeVariant : '';
+
+// Set property visibility
+trackLabel.style.display = showPrimary ? '' : 'none';
+artistLabel.style.display = showSecondary ? '' : 'none';
+
+// Set container width
+if (maxWidth > 0)
+ mainWrapper.style.width = `${maxWidth}px`;
+else
+ mainWrapper.style.width = `100%`;
+
+// Set progress bar visibility
+if (!showProgressBar)
+ progressContainer.style.display = 'none';
+
+// Theme specific setup
+switch (themeVariant) {
+ case "matte":
+ case "matte-dark":
+ backgroundLayer.style.display = 'none';
+ backgroundTransitionLayer.style.display = 'none';
+ break;
+}
+
+// If text alignment is 'right', swap the album art to the right hand side too
+if (textAlignment == 'right') {
+ songInfoContainer.style.flexDirection = 'row-reverse';
+}
+
+
+////////////////////
+// CORE FUNCTIONS //
+////////////////////
+
+async function ChangeTrack(mediaProps, accentColorPalette) {
+ // Fade in the overlay (shows the new text)
+ if (trackLabel.innerText != (swapArtistTrack ? mediaProps.Artist : mediaProps.Title))
+ trackLabel.style.opacity = "0";
+ if (artistLabel.innerText != (swapArtistTrack ? mediaProps.Title : mediaProps.Artist))
+ artistLabel.style.opacity = "0";
+ backgroundLayer.style.opacity = "0";
+ albumArtLayer.style.opacity = "0";
+
+ // Wait for fade (0.5s), then swap the real text and hide overlay
+ setTimeout(async () => {
+ SetLabelText('track-label', swapArtistTrack ? mediaProps.Artist : mediaProps.Title);
+ SetLabelText('artist-label', swapArtistTrack ? mediaProps.Title : mediaProps.Artist);
+
+ if (!useCustomColors)
+ {
+ switch (themeVariant) {
+ case "matte":
+ document.body.style.color = accentColorPalette.DarkVibrant;
+ break;
+ case "matte-dark":
+ document.body.style.color = accentColorPalette.LightVibrant;
+ break;
+ }
+ }
+ else
+ {
+ switch (themeVariant) {
+ case "matte":
+ document.body.style.color = color2;
+ break;
+ // case "matte-dark":
+ default:
+ document.body.style.color = color1;
+ break;
+ }
+ }
+
+ // Extract the image source string (use fallback if Windows has no art)
+ const newArtUrl = mediaProps.Thumbnail ?? './images/placeholder.png';
+
+ // Set the image
+ if (!useCustomColors) {
+ switch (themeVariant) {
+ case "matte":
+ songInfoContainer.style.backgroundColor = accentColorPalette.LightVibrant;
+ break;
+ case "matte-dark":
+ songInfoContainer.style.backgroundColor = `color-mix(in srgb, ${accentColorPalette.DarkMuted}, black 60%)`;
+ break;
+ }
+ }
+ else
+ {
+ switch (themeVariant) {
+ case "matte":
+ songInfoContainer.style.backgroundColor = color1;
+ break;
+ // case "matte-dark":
+ default:
+ songInfoContainer.style.backgroundColor = `color-mix(in srgb, ${color2}, black 60%)`;
+ break;
+ }
+ }
+ backgroundLayer.style.backgroundImage = `url('${newArtUrl}')`;
+ albumArtLayer.style.backgroundImage = `url('${newArtUrl}')`;
+
+ // Apply a tint
+ backgroundLayer.style.backgroundColor = accentColorPalette.DarkMuted + "80"; // 80 is 50% opacity in hex
+
+ trackLabel.style.opacity = "";
+ artistLabel.style.opacity = "";
+ backgroundLayer.style.opacity = "";
+ albumArtLayer.style.opacity = "";
+
+ setTimeout(() => {
+ // Set the image
+ backgroundTransitionLayer.style.backgroundImage = `url('${newArtUrl}')`;
+ albumArtTransition.style.backgroundImage = `url('${newArtUrl}')`;
+
+ // Apply a tint
+ backgroundTransitionLayer.style.backgroundColor = accentColorPalette.DarkMuted + "80"; // 80 is 50% opacity in hex
+
+ SetVisibility(true); // Show the overlay for a few seconds if autoHide is enabled
+ }, 250);
+ }, 250);
+}
+
+function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
+ // Update the label using your naming convention
+ currentTimeLabel.innerText =
+ ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs);
+
+ durationLabel.innerText =
+ ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime);
+
+ // Set progressbar
+ // Ensure we don't divide by zero or exceed 100%
+ const durationMs = timelineProps.EndTime;
+ let progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0;
+ progressPercent = Math.min(100, Math.max(0, progressPercent));
+ progressBarFill.style.width = `${progressPercent}%`;
+ progressBarFill.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
+ if (!useCustomColors) {
+ switch (themeVariant) {
+ case "matte":
+ progressBarFill.style.setProperty('--accent-color', accentColorPalette.DarkVibrant);
+ break;
+ case "matte-dark":
+ default:
+ progressBarFill.style.setProperty('--accent-color', accentColorPalette.LightVibrant);
+ break;
+ }
+ }
+ else
+ {
+ switch (themeVariant) {
+ case "matte":
+ progressBarFill.style.setProperty('--accent-color', color2);
+ break;
+ // case "matte-dark":
+ default:
+ progressBarFill.style.setProperty('--accent-color', color1);
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/now-playing/themes/standard/style.css b/now-playing/themes/standard/style.css
new file mode 100644
index 0000000..90959e8
--- /dev/null
+++ b/now-playing/themes/standard/style.css
@@ -0,0 +1,149 @@
+:root {
+ --border-radius: 20px;
+ --component-radius: calc(var(--border-radius) * 0.75);
+}
+
+#main-wrapper {
+ width: 500px;
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ gap: 0.5em;
+ opacity: 0;
+}
+
+#album-art-container {
+ /* 1. Tell it to maintain a 1:1 square ratio */
+ height: 100%;
+ aspect-ratio: 1 / 1;
+ overflow: hidden;
+ border-radius: var(--component-radius);
+ position: relative;
+ background-size: cover;
+ background-position: center;
+ display: var(--show-album-art);
+ flex-shrink: 0;
+}
+
+#album-art-layer,
+#album-art-transition-layer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-size: cover;
+ background-position: center;
+ transition: opacity 0.5s ease;
+}
+
+#album-art-layer {
+ z-index: 1;
+ transition: all 0.5s ease;
+}
+
+#album-art-transition-layer {
+ z-index: 0;
+}
+
+#song-info-container {
+ position: relative;
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ border-radius: var(--border-radius);
+ overflow: hidden;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
+ gap: 1.5em;
+ padding: 0.75em 1em;
+ transition: all 0.25s ease-in-out;
+}
+
+/* The background now fills the frame */
+#background-layer,
+#background-transition-layer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ background-size: cover;
+ background-position: center center;
+ background-repeat: no-repeat;
+
+ background-blend-mode: overlay; /* Or 'soft-light' for a subtler effect */
+ filter: blur(10px) brightness(0.3) saturate(1.2);
+ transform: scale(1.1); /* Slightly zoom in to cover edges when blurred */
+}
+
+#background-layer {
+ transition: all 0.5s ease;
+}
+
+#background-transition-layer {
+ z-index: 0;
+}
+
+/* The content sits on top */
+#song-info-wrapper {
+ position: relative; /* Keeps it in the document flow */
+ z-index: 2; /* Sits above the background-layer */
+
+ /* padding: 1em 1.75em; */
+ display: flex;
+ flex-direction: column;
+ gap: 0.3em;
+
+ margin: 0.5em 0em;
+
+ flex-grow: 1;
+ min-width: 0;
+}
+
+#track-label {
+ font-size: 1em;
+ font-weight: 600;
+ text-align: var(--text-alignment);
+}
+
+#artist-label {
+ font-size: 0.8em;
+ font-weight: 300;
+ opacity: 0.6;
+ text-align: var(--text-alignment);
+}
+
+#progress-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 0.2em;
+ margin-top: 0.2em;
+ gap: 0.5em;
+}
+
+#progress-bar-track {
+ width: 100%;
+ height: 0.4em;
+ border-radius: 1em;
+ background-color: #ffffff3a;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); /* Adds depth */
+}
+
+#progress-bar-fill {
+ height: 100%;
+ width: 0%; /* JS will control this */
+ border-radius: 1em;
+ background-color: var(--accent-color, #ffffff);
+ transition: width 1s ease-in-out, background-color 1s ease-in-out;
+
+ /* This makes the handle track the end of the bar */
+ position: relative;
+}
+
+.progress-text {
+ font-size: 0.7em;
+ opacity: 0.5;
+}
\ No newline at end of file
diff --git a/now-playing/themes/vinyl/images/vinyl.af b/now-playing/themes/vinyl/images/vinyl.af
new file mode 100644
index 0000000..e5c8f12
Binary files /dev/null and b/now-playing/themes/vinyl/images/vinyl.af differ
diff --git a/now-playing/themes/vinyl/images/vinyl.png b/now-playing/themes/vinyl/images/vinyl.png
new file mode 100644
index 0000000..68f85ee
Binary files /dev/null and b/now-playing/themes/vinyl/images/vinyl.png differ
diff --git a/now-playing/themes/vinyl/index.html b/now-playing/themes/vinyl/index.html
new file mode 100644
index 0000000..2b39992
--- /dev/null
+++ b/now-playing/themes/vinyl/index.html
@@ -0,0 +1,12 @@
+
+
+
+

+
+
+
\ No newline at end of file
diff --git a/now-playing/themes/vinyl/script.js b/now-playing/themes/vinyl/script.js
new file mode 100644
index 0000000..c4d2d48
--- /dev/null
+++ b/now-playing/themes/vinyl/script.js
@@ -0,0 +1,78 @@
+///////////////////
+// PAGE ELEMENTS //
+///////////////////
+
+const mainWrapper = document.getElementById('main-wrapper');
+const albumArtContainer = document.getElementById('album-art-container');
+// const songInfoContainer = document.getElementById('song-info-container');
+const albumArtLayer = document.getElementById('album-art-layer');
+const albumArtTransition = document.getElementById('album-art-transition-layer');
+const progressCircle = document.getElementById('progress-pie-circle');
+
+///////////////
+// CONSTANTS //
+///////////////
+
+const circumference = 157.1;
+
+
+
+
+////////////////
+// PAGE SETUP //
+////////////////
+
+// Set container width
+if (maxWidth > 0)
+ mainWrapper.style.width = `${maxWidth}px`;
+else
+ mainWrapper.style.width = `100%`;
+
+// Set progress bar visibility
+if (!showProgressBar)
+ progressCircle.style.display = 'none';
+
+
+
+////////////////////
+// CORE FUNCTIONS //
+////////////////////
+
+async function ChangeTrack(mediaProps) {
+ // // Fade in the overlay (shows the new text)
+ albumArtLayer.style.opacity = "0";
+
+ // Wait for fade (0.5s), then swap the real text and hide overlay
+ setTimeout(() => {
+
+ // Extract the image source string (use fallback if Windows has no art)
+ const newArtUrl = mediaProps.Thumbnail ?? './images/placeholder.png';
+
+ // Set the image
+ albumArtLayer.style.backgroundImage = `url('${newArtUrl}')`;
+ albumArtLayer.style.opacity = "";
+
+ setTimeout(() => {
+ // Set the image
+ albumArtTransition.style.backgroundImage = `url('${newArtUrl}')`;
+ SetVisibility(true); // Show the overlay for a few seconds if autoHide is enabled
+ }, 250);
+ }, 250);
+}
+
+function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
+ // Set progressbar
+ // Ensure we don't divide by zero or exceed 100%
+ const durationMs = timelineProps.EndTime;
+ progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0;
+ progressPercent = Math.min(100, Math.max(0, progressPercent));
+ if (!useCustomColors)
+ document.documentElement.style.setProperty('--accent-color', `${accentColorPalette.LightVibrant}`);
+ else
+ document.documentElement.style.setProperty('--accent-color', color1);
+
+ // Calculate the offset.
+ // 0% progress = 157.1 offset. 100% progress = 0 offset.
+ const offset = circumference - (progressPercent / 100) * circumference;
+ progressCircle.style.strokeDashoffset = offset;
+}
\ No newline at end of file
diff --git a/now-playing/themes/vinyl/style.css b/now-playing/themes/vinyl/style.css
new file mode 100644
index 0000000..cce72c6
--- /dev/null
+++ b/now-playing/themes/vinyl/style.css
@@ -0,0 +1,97 @@
+
+
+#main-wrapper {
+ width: 500px;
+ display: flex;
+ flex-direction: row;
+ gap: 1em;
+ opacity: 0;
+ align-items: center;
+ justify-content: center;
+}
+
+#vinyl-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ filter: drop-shadow(0px 10px 15px rgba(0, 0, 0, 0.5));
+ animation: spin 10s linear infinite;
+}
+
+#vinyl-background {
+ position: relative;
+ height: 100%;
+ width: 100%;
+}
+
+#album-art-container {
+ position: absolute;
+ width: 35%;
+ height: 35%;
+ border-radius: 50%;
+ overflow: hidden;
+
+ -webkit-mask-image: radial-gradient(circle, transparent 3%, black 3.5%);
+ mask-image: radial-gradient(circle, transparent 3%, black 3.5%);
+}
+
+#album-art-layer,
+#album-art-transition-layer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-size: cover;
+ background-position: center;
+ transition: opacity 0.5s ease;
+ display: var(--show-album-art);
+}
+
+#album-art-layer {
+ z-index: 2;
+ transition: all 0.5s ease;
+}
+
+#album-art-transition-layer {
+ z-index: 1;
+}
+
+#progress-ring-container {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ border-radius: 20%;
+ overflow: hidden; /* This masks the 'pie slice' */
+ z-index: 0;
+}
+
+#progress-svg {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ /* Rotates the start point to 12 o'clock */
+ transform: rotate(-90deg);
+ -webkit-mask-image: radial-gradient(circle, transparent 3%, black 3.5%);
+ mask-image: radial-gradient(circle, transparent 3%, black 3.5%);
+}
+
+#progress-pie-circle {
+ fill: transparent;
+ stroke: var(--accent-color); /* Falls back to red if var fails */
+ stroke-width: 50; /* This thickness makes it a solid pie */
+
+ /* 157.1 is the exact circumference of a circle with r=25 (2 * PI * 25) */
+ stroke-dasharray: 157.1;
+ stroke-dashoffset: 157.1; /* Starts empty */
+
+ /* This creates the smooth, non-stuttering animation */
+ transition: all 1s linear;
+}
+
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
\ No newline at end of file
diff --git a/printer-bot/contents/script.js b/printer-bot/contents/script.js
index ccc444d..5fa6563 100644
--- a/printer-bot/contents/script.js
+++ b/printer-bot/contents/script.js
@@ -192,7 +192,7 @@ async function CustomEvent(data) {
case ('TwitchCustomPowerUpRedemption'):
{
avatarEl.src = await GetAvatar(data.userName, 'twitch');
- titleEl.innerText = `${data["customPowerUp.bits"]} BITS`;
+ titleEl.innerText = `${data["customPowerUp.bitsCost"]} BITS`;
subtitleEl.innerText = `${data.user}`;
const messageEl = document.createElement('div');