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 + ? `

+ Alert Icon +
` + : ''; + + const titleMarkup = title + ? `` + : ''; + + const subtitleMarkup = subtitle + ? `` + : ''; + + const attributeMarkup = attribute + ? `` + : ''; + + // Render button conditional markup + const buttonMarkup = (button && button.text) + ? `` + : ''; + + card.innerHTML = ` + ${iconMarkup} +
+ ${titleMarkup} + ${subtitleMarkup} + ${attributeMarkup} + ${buttonMarkup} +
+ `; + + // 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 @@ +
+ +
+
+
+ +
+ +
+
+
+
+ +
 
+
 
+ +
+
+
+
+
+ +
+
0:00
+
0:00
+
+
+
+
+
\ 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 @@ +
+
+
+
+
+
+ +
+
+
+ +
+
 
+
 
+ +
+
+
+
+
+ +
+
0:00
+
0:00
+
+
+
+
+
+
\ 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 @@ +
+
+ +
+
+ Vibrant + +
+
+ + +
+
+ Muted + +
+
+ + +
+
+ Dark Vibrant + +
+
+ + +
+
+ Dark Muted + +
+
+ + +
+
+ Light Vibrant + +
+
+ + +
+
+ Light Muted + +
+
+
+
\ 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 @@ +
+
+
+
+
+ +
+
 
+
 
+
+
0:00
+
+
+
+
+
0:00
+
+
+
\ 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 @@ +
+
+
+
+ +
+
+
+
+ +
+ +
 
+
 
+ +
+
0:00
+
+
+
+
+
0:00
+
+
+
+
\ 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');