mirror of
https://github.com/nuttylmao/nutty.gg.git
synced 2026-09-18 19:50:58 -04:00
@@ -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 <p> tag for this setting in the DOM and overwrite its HTML
|
||||
const descriptionParagraph = document.querySelector(`#item-${setting.id} p`);
|
||||
if (descriptionParagraph) {
|
||||
descriptionParagraph.innerHTML = dynamicHTML;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -495,3 +593,6 @@ LoadSettingsFromStorage();
|
||||
|
||||
// Load default settings
|
||||
LoadJSON(settingsJson);
|
||||
|
||||
// Populate local fonts for auto-suggest
|
||||
PopulateFontDatalist();
|
||||
@@ -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
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 380 KiB |
@@ -412,3 +412,301 @@ async function LoadHTMLTemplate(name) {
|
||||
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
|
||||
? `<div class="common-popup-icon-container">
|
||||
<img src="${iconSrc}" class="common-popup-img" alt="Alert Icon" />
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
const titleMarkup = title
|
||||
? `<label class="common-popup-title">${title}</label>`
|
||||
: '';
|
||||
|
||||
const subtitleMarkup = subtitle
|
||||
? `<label class="common-popup-subtitle">${subtitle}</label>`
|
||||
: '';
|
||||
|
||||
const attributeMarkup = attribute
|
||||
? `<label class="common-popup-attribute">${attribute}</label>`
|
||||
: '';
|
||||
|
||||
// Render button conditional markup
|
||||
const buttonMarkup = (button && button.text)
|
||||
? `<button class="common-popup-button">${button.text}</button>`
|
||||
: '';
|
||||
|
||||
card.innerHTML = `
|
||||
${iconMarkup}
|
||||
<div class="common-popup-text-content">
|
||||
${titleMarkup}
|
||||
${subtitleMarkup}
|
||||
${attributeMarkup}
|
||||
${buttonMarkup}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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';
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="main-container">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<label id="line1" class="timeLabel"></label>
|
||||
<label id="line2" class="timeLabel"></label>
|
||||
<label id="line3" class="timeLabel"></label>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="/.common/utils/helpers.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/utc.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/timezone.min.js"></script>'
|
||||
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/advancedFormat.js"></script>'
|
||||
<script src="./script.js"></script>
|
||||
@@ -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);
|
||||
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="/.common/resources/logo.png" type="image/png">
|
||||
<title>nutty</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
background: #181818;
|
||||
}
|
||||
|
||||
#widgetContainer {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
border-width: 0px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<iframe id="widgetContainer"></iframe>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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;
|
||||
@@ -0,0 +1,306 @@
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"id": "font",
|
||||
"label": "Font",
|
||||
"description": "<a href=\"ms-settings:fonts\">Check installed fonts</a>",
|
||||
"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": "<a href='https://day.js.org/docs/en/display/format' target='_blank'>Click here for formatting options</a>",
|
||||
"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": "<a href='https://day.js.org/docs/en/display/format' target='_blank'>Click here for formatting options</a>",
|
||||
"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": "<a href='https://day.js.org/docs/en/display/format' target='_blank'>Click here for formatting options</a>",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -59,8 +59,8 @@
|
||||
{
|
||||
"id": "font",
|
||||
"label": "Font",
|
||||
"description": "",
|
||||
"type": "text",
|
||||
"description": "<a href=\"ms-settings:fonts\">Check installed fonts</a>",
|
||||
"type": "font",
|
||||
"defaultValue": "",
|
||||
"group": "Appearance"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -59,8 +59,8 @@
|
||||
{
|
||||
"id": "font",
|
||||
"label": "Font",
|
||||
"description": "",
|
||||
"type": "text",
|
||||
"description": "<a href=\"ms-settings:fonts\">Check installed fonts</a>",
|
||||
"type": "font",
|
||||
"defaultValue": "",
|
||||
"group": "Appearance"
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
{
|
||||
"id": "font",
|
||||
"label": "Font",
|
||||
"description": "",
|
||||
"type": "text",
|
||||
"description": "<a href=\"ms-settings:fonts\">Check installed fonts</a>",
|
||||
"type": "font",
|
||||
"defaultValue": "",
|
||||
"group": "Appearance"
|
||||
},
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Now Playing</title>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<link id="theme-style" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="main-container">
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.2/color-thief.min.js"></script>
|
||||
<script src="/.common/utils/helpers.js"></script>
|
||||
<script src="/.common/enums/smtc-enums.js"></script>
|
||||
<script id="theme-script" src="./script.js"></script>
|
||||
</html>
|
||||
@@ -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.',
|
||||
`<b>Installed Version: ${installedVersion}</b><br><b>New Version: ${REQUIRED_VERSION}</b>`,
|
||||
'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.',
|
||||
`<b>Installed Version: ${installedVersion}</b><br><b>New Version: ${REQUIRED_VERSION}</b>`,
|
||||
'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 = `<span class="scroll-content"></span>`;
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="/.common/resources/logo.png" type="image/png">
|
||||
<title>nutty</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
background: #181818;
|
||||
}
|
||||
|
||||
#widgetContainer {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
border-width: 0px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<iframe id="widgetContainer"></iframe>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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;
|
||||
@@ -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": "<a href=\"ms-settings:fonts\">Check installed fonts</a>",
|
||||
"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.<br>Leave empty to track currently focused app.<br><a href='http://{smtcBridgeAddress}:{smtcBridgePort}/sessions' target='_blank' style='color: #4da6ff; text-decoration: underline;'>View active sources</a>",
|
||||
"type": "text",
|
||||
"placeholder": "Spotify.exe, vlc.exe",
|
||||
"defaultValue": "",
|
||||
"group": "General"
|
||||
},
|
||||
{
|
||||
"id": "excludedApplications",
|
||||
"label": "Excluded Apps",
|
||||
"description": "Apps listed here will not be displayed.<br><a href='http://{smtcBridgeAddress}:{smtcBridgePort}/sessions' target='_blank' style='color: #4da6ff; text-decoration: underline;'>View active sources</a>",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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: '<color>';
|
||||
inherits: false;
|
||||
initial-value: black;
|
||||
}
|
||||
|
||||
@property --mask-right {
|
||||
syntax: '<color>';
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<div id="main-wrapper">
|
||||
<div id="song-info-container">
|
||||
<div id="album-art-container">
|
||||
<div id="album-art-layer"></div>
|
||||
<div id="album-art-transition-layer"></div>
|
||||
</div>
|
||||
|
||||
<div id="text-info-container">
|
||||
<div id="artist-label" class="text-label"> </div>
|
||||
<div id="track-label" class="text-label"> </div>
|
||||
</div>
|
||||
|
||||
<div id="progress-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<div id="main-wrapper">
|
||||
|
||||
<div id="song-info-container">
|
||||
<div id="background-layer"></div>
|
||||
<div id="background-transition-layer"></div>
|
||||
|
||||
<div id="song-info-wrapper">
|
||||
|
||||
<div id="album-art-container">
|
||||
<div id="album-art-layer"></div>
|
||||
<div id="album-art-transition-layer"></div>
|
||||
</div>
|
||||
|
||||
<div id="track-label" class="text-label"> </div>
|
||||
<div id="artist-label" class="text-label"> </div>
|
||||
|
||||
<div id="progress-container">
|
||||
<div id="progress-bar-track">
|
||||
<div id="progress-bar-fill"></div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<div id="time-container">
|
||||
<div id="current-time">0:00</div>
|
||||
<div id="duration">0:00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<div id="main-wrapper">
|
||||
<div id="yet-another-wrapper-yup">
|
||||
<div id="album-art-container">
|
||||
<div id="album-art-layer"></div>
|
||||
<div id="album-art-transition-layer"></div>
|
||||
</div>
|
||||
|
||||
<div id="song-info-container">
|
||||
<div id="background-layer"></div>
|
||||
<div id="background-transition-layer"></div>
|
||||
|
||||
<div id="song-info-wrapper">
|
||||
<div id="track-label" class="text-label"> </div>
|
||||
<div id="artist-label" class="text-label"> </div>
|
||||
|
||||
<div id="progress-container">
|
||||
<div id="progress-bar-track">
|
||||
<div id="progress-bar-fill"></div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<div id="time-container">
|
||||
<div id="current-time">0:00</div>
|
||||
<div id="duration">0:00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<div id="main-wrapper">
|
||||
<div id="color-palette">
|
||||
<!-- Vibrant -->
|
||||
<div id="box-vibrant" class="color-box">
|
||||
<div class="color-info">
|
||||
<span class="secondary-text">Vibrant</span>
|
||||
<span id="label-vibrant" class="primary-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Muted -->
|
||||
<div id="box-muted" class="color-box">
|
||||
<div class="color-info">
|
||||
<span class="secondary-text">Muted</span>
|
||||
<span id="label-muted" class="primary-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dark Vibrant -->
|
||||
<div id="box-dark-vibrant" class="color-box">
|
||||
<div class="color-info">
|
||||
<span class="secondary-text">Dark Vibrant</span>
|
||||
<span id="label-dark-vibrant" class="primary-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dark Muted -->
|
||||
<div id="box-dark-muted" class="color-box">
|
||||
<div class="color-info">
|
||||
<span class="secondary-text">Dark Muted</span>
|
||||
<span id="label-dark-muted" class="primary-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Light Vibrant -->
|
||||
<div id="box-light-vibrant" class="color-box">
|
||||
<div class="color-info">
|
||||
<span class="secondary-text">Light Vibrant</span>
|
||||
<span id="label-light-vibrant" class="primary-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Light Muted -->
|
||||
<div id="box-light-muted" class="color-box">
|
||||
<div class="color-info">
|
||||
<span class="secondary-text">Light Muted</span>
|
||||
<span id="label-light-muted" class="primary-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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}%`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"base-theme": "compact"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div id="main-wrapper">
|
||||
<div id="song-info-container">
|
||||
<div id="progress-bar-track" class="progress">
|
||||
<label id="song-label-background" class="text-label"></label>
|
||||
</div>
|
||||
|
||||
<div id="progress-bar" class="progress">
|
||||
<label id="song-label" class="text-label"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 = `
|
||||
<span class="scroll-content">
|
||||
<span class="track-label">${trackName}</span>
|
||||
<span class="artist-label">${artistName}</span>
|
||||
</span>
|
||||
`;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"base-theme": "standard"
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"base-theme": "standard"
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<div id="main-wrapper">
|
||||
<div id="album-art-container">
|
||||
<div id="album-art-layer"></div>
|
||||
<div id="album-art-transition-layer"></div>
|
||||
</div>
|
||||
|
||||
<div id="song-info-container">
|
||||
<div id="track-label" class="text-label"> </div>
|
||||
<div id="artist-label" class="text-label"> </div>
|
||||
<div id="progress-container">
|
||||
<div id="current-time" class="progress-text">0:00</div>
|
||||
<div id="progress-bar-track">
|
||||
<div id="progress-bar-fill"></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div id="duration" class="progress-text">0:00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<div id="main-wrapper">
|
||||
<div id="song-info-container">
|
||||
<div id="background-layer"></div>
|
||||
<div id="background-transition-layer"></div>
|
||||
|
||||
<div id="album-art-container">
|
||||
<div id="album-art-layer"></div>
|
||||
<div id="album-art-transition-layer"></div>
|
||||
</div>
|
||||
|
||||
<div id="song-info-wrapper">
|
||||
|
||||
<div id="track-label" class="text-label"> </div>
|
||||
<div id="artist-label" class="text-label"> </div>
|
||||
|
||||
<div id="progress-container">
|
||||
<div id="current-time" class="progress-text">0:00</div>
|
||||
<div id="progress-bar-track">
|
||||
<div id="progress-bar-fill"></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div id="duration" class="progress-text">0:00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 271 KiB |
@@ -0,0 +1,12 @@
|
||||
<div id="main-wrapper">
|
||||
<svg id="progress-svg" viewBox="0 0 100 100">
|
||||
<circle id="progress-pie-circle" cx="50" cy="50" r="25"></circle>
|
||||
</svg>
|
||||
<div id="vinyl-container">
|
||||
<img id="vinyl-background" src="/now-playing/themes/vinyl/images/vinyl.png">
|
||||
<div id="album-art-container">
|
||||
<div id="album-art-layer"></div>
|
||||
<div id="album-art-transition-layer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user