diff --git a/now-playing/index.html b/now-playing/index.html index cd5baf3..8dce8ef 100644 --- a/now-playing/index.html +++ b/now-playing/index.html @@ -4,12 +4,12 @@ Now Playing - +
-
+
- + \ No newline at end of file diff --git a/now-playing/script.js b/now-playing/script.js index 7a03123..c7a811f 100644 --- a/now-playing/script.js +++ b/now-playing/script.js @@ -10,26 +10,7 @@ const urlParams = new URLSearchParams(queryString); // PAGE ELEMENTS // /////////////////// -const standardLayout = document.getElementById('standard-layout'); -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'); - -///////////////// -// GLOBAL VARS // -///////////////// - -let CurrentPlaybackStatus; -let CurrentSong; +const mainContainer = document.getElementById('main-container'); ///////////// // OPTIONS // @@ -51,30 +32,34 @@ const hideAnimation = urlParams.get('hideAnimation') || 'fade'; //////////////// -// PAGE SETUP // +// LOAD THEME // //////////////// -// Set fonts for the widget -document.body.style.fontFamily = font; -document.body.style.fontSize = `${fontSize}px`; -standardLayout.style.width = `${width}px`; - -// Set album art style -switch (albumArt) +// Load the theme +async function LoadTheme() { - case 'none': - albumArtContainer.style.display = 'none'; - break; - case 'show': - albumArtContainer.style.display = ''; - break; - case 'spinny': - break; + const response = await fetch(`./themes/${theme}/index.html`); + const html = await response.text(); + + mainContainer.innerHTML = html; + + // Swap the CSS file + const link = document.getElementById('theme-style'); + link.href = `./themes/${theme}/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/${theme}/script.js`; + script.id = 'theme-script'; + document.body.appendChild(script); } -// Set progress bar visibility -if (!showProgressBar) - progressContainer.style.display = 'none'; +LoadTheme(); + ///////////////// @@ -87,88 +72,14 @@ async function FetchMedia() { const data = await response.json(); // Update the UI with the received data - console.log(data); - UpdateUI(data); + // console.log(data); + UpdatePlayerState(data); } catch (error) { console.error("Failed to connect to Flask media server:", error); } } -function UpdateUI(data) { - // Check if the user has provided a specific app in the settings - const isFiltering = targetApplication && targetApplication.trim() !== ""; - - // Decide which ID to look for - const sessionToFind = isFiltering ? targetApplication : data.current_session_id; - - // Now perform the find - let targetSession = data.sessions.find(s => { - // If filtering, check if the source_app_id matches the user's string - if (isFiltering) { - return s.source_app_id.toLowerCase() === targetApplication.toLowerCase(); - } - // Otherwise, match the system's current active session ID - return s.source_app_id === sessionToFind; - }); - - if (targetSession) { - // Extract the relevant properties from the session - const playbackInfo = targetSession.playback_info; - const mediaProps = targetSession.media_properties; - const timelineProps = targetSession.timeline_properties; - - // 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; - } - - // Save current media properties - we can use this to detect track changes and trigger transition animations in the future - const newTrackKey = `${mediaProps.Title}|${mediaProps.Artist}`; - if (newTrackKey !== CurrentSong) { - ChangeTrack(mediaProps); // Now trigger your cross-fade logic here! - CurrentSong = newTrackKey; // Update the tracker with the string key - } - - if (timelineProps) - { - // Parse the Windows timestamp into a JavaScript time object - const lastUpdateAnchor = Date.parse(timelineProps.LastUpdatedTime.replace(' ', 'T')); - - // Calculate the drift: how many milliseconds have passed since Windows last spoke? - const driftMs = Date.now() - lastUpdateAnchor; - - // Add that drift to the reported Position - // Only add drift if the status is 4 (Playing) - const isPlaying = (targetSession.playback_info.PlaybackStatus === 4); - const currentPositionMs = isPlaying && timelineProps.EndTime > 0 ? timelineProps.Position + driftMs : timelineProps.Position; - - // Update the label using your naming convention - currentTimeLabel.innerText = - ConvertMillisecondsToMinutesSoThatItLooksBetterOnTheOverlay(currentPositionMs); - - durationLabel.innerText = - ConvertMillisecondsToMinutesSoThatItLooksBetterOnTheOverlay(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', mediaProps.AccentColor); - } - } - else - { - standardLayout.style.opacity = "0"; - } -} - // Start polling every 1000 milliseconds setInterval(FetchMedia, 1000); @@ -181,85 +92,6 @@ FetchMedia(); // HELPER FUNCTIONS // ////////////////////// -let hideTimeout = null; // Store the timer in a global/outer variable - -function SetVisibility(visible) { - // Always clear any pending hide timers whenever we change visibility - if (hideTimeout) { - clearTimeout(hideTimeout); - hideTimeout = null; - } - - if (visible) { - standardLayout.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 { - standardLayout.style.animation = `${hideAnimation} 0.5s ease-out forwards`; - } -} - -async function ChangeTrack(mediaProps) { - // Fade in the overlay (shows the new text) - trackLabel.style.opacity = "0"; - 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(() => { - trackLabel.innerText = mediaProps.Title; - artistLabel.innerText = mediaProps.Artist; - - // Extract the image source string (use fallback if Windows has no art) - const newArtUrl = mediaProps.Base64Image; - const accent = mediaProps.AccentColor || "#ffffff"; - - // Set the image - backgroundLayer.style.backgroundImage = `url('${newArtUrl}')`; - albumArtLayer.style.backgroundImage = `url('${newArtUrl}')`; - - // Apply a tint: Use 30% opacity of the accent color + a dark overlay for contrast - // 'rgba(0,0,0,0.6)' ensures the text stays readable - backgroundLayer.style.backgroundColor = accent + "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: Use 30% opacity of the accent color + a dark overlay for contrast - // 'rgba(0,0,0,0.6)' ensures the text stays readable - backgroundTransitionLayer.style.backgroundColor = accent + "80"; // 80 is 50% opacity in hex - - SetVisibility(true); // Show the overlay for a few seconds if autoHide is enabled - }, 250); - }, 250); -} - -window.addEventListener('resize', syncHeights); -syncHeights(); - -function syncHeights() { - requestAnimationFrame(() => { - const height = songInfoContainer.offsetHeight; - console.log(height); - if (height > 0) { - albumArtContainer.style.height = `${height}px`; - } - }); -} - function ConvertMillisecondsToMinutesSoThatItLooksBetterOnTheOverlay(time) { if (isNaN(time) || time <= 0) return "0:00"; diff --git a/now-playing/settings/settings.json b/now-playing/settings/settings.json index a7ac0c0..d0dccaf 100644 --- a/now-playing/settings/settings.json +++ b/now-playing/settings/settings.json @@ -63,10 +63,6 @@ { "value": "show", "label": "Show" - }, - { - "value": "spinny", - "label": "Spinny disc thingy cause that looks dope as hell" } ], "defaultValue": "none", diff --git a/now-playing/style.css b/now-playing/style.css index 0ae485f..e69de29 100644 --- a/now-playing/style.css +++ b/now-playing/style.css @@ -1,303 +0,0 @@ -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; /* Full viewport height */ -} - -#standard-layout { - width: 500px; - display: flex; - flex-direction: row; - /* This forces both containers to be the same height */ - align-items: stretch; - gap: 0.5em; -} - -#album-art-container { - /* 1. Tell it to maintain a 1:1 square ratio */ - aspect-ratio: 1 / 1; - overflow: hidden; - border-radius: 1em; - position: relative; - background-size: cover; - background-position: center; -} - -#art-layer, -#art-transition-layer { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-size: cover; - background-position: center; - transition: opacity 0.5s ease; -} - -#art-layer { - z-index: 1; - transition: all 0.5s ease; -} - -#art-transition-layer { - z-index: 0; -} - -#song-info-container { - position: relative; - flex: 1; - display: flex; - flex-direction: column; - border-radius: 1em; - 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(0px) 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: 1em 1.75em; - display: flex; - flex-direction: column; - gap: 0.3em; -} - -#track-label { - font-size: 1em; - font-weight: 600; -} - -#artist-label { - font-size: 0.8em; - font-weight: 300; - opacity: 0.6; -} - -.text-label { - white-space: nowrap; - overflow: hidden; - mask-image: linear-gradient(to right, black calc(100% - 1em), transparent 100%); - -webkit-mask-image: linear-gradient(to right, black calc(100% - 1em), transparent 100%); - 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: 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-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; -} - -@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; - } -} \ No newline at end of file diff --git a/now-playing/themes/standard/index.html b/now-playing/themes/standard/index.html new file mode 100644 index 0000000..0e26862 --- /dev/null +++ b/now-playing/themes/standard/index.html @@ -0,0 +1,32 @@ + + + + +
+
+
+
+
+ +
+
+
+ +
+
 
+
 
+ +
+
+
+
+
+ +
+
0:00
+
0:00
+
+
+
+
+
\ No newline at end of file diff --git a/now-playing/themes/standard/script.js b/now-playing/themes/standard/script.js new file mode 100644 index 0000000..c2738ad --- /dev/null +++ b/now-playing/themes/standard/script.js @@ -0,0 +1,212 @@ +///////////////// +// GLOBAL VARS // +///////////////// + +let CurrentPlaybackStatus; +let CurrentSong; +let hideTimeout = null; + +/////////////////// +// PAGE ELEMENTS // +/////////////////// + +const standardLayout = document.getElementById('standard-layout'); +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 fonts for the widget +document.body.style.fontFamily = font; +document.body.style.fontSize = `${fontSize}px`; +standardLayout.style.width = `${width}px`; + +// Set album art style +switch (albumArt) +{ + case 'none': + albumArtContainer.style.display = 'none'; + break; + case 'show': + albumArtContainer.style.display = ''; + break; + case 'spinny': + break; +} + +// Set progress bar visibility +if (!showProgressBar) + progressContainer.style.display = 'none'; + + + +//////////////////// +// CORE FUNCTIONS // +//////////////////// + +// Each theme must implement the following +function UpdatePlayerState(data) { + // Check if the user has provided a target application in the settings + const isFiltering = targetApplication && targetApplication.trim() !== ""; + + // Search for the session that matches the target application + const sessionToFind = isFiltering ? targetApplication : data.current_session_id; + let targetSession = data.sessions.find(s => { + // If filtering, check if the source_app_id matches the user's string + if (isFiltering) { + return s.source_app_id.toLowerCase() === targetApplication.toLowerCase(); + } + // Otherwise, match the system's current active session ID + return s.source_app_id === sessionToFind; + }); + + // 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; + + // 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 + const newTrackKey = `${mediaProps.Title}|${mediaProps.Artist}`; + if (newTrackKey !== CurrentSong) { + ChangeTrack(mediaProps); // Now trigger your cross-fade logic here! + CurrentSong = 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; + + // Update the label using your naming convention + currentTimeLabel.innerText = + ConvertMillisecondsToMinutesSoThatItLooksBetterOnTheOverlay(currentPositionMs); + + durationLabel.innerText = + ConvertMillisecondsToMinutesSoThatItLooksBetterOnTheOverlay(timelineProps.EndTime); + + // Set progressbar + // Ensure we don't divide by zero or exceed 100% + const durationMs = timelineProps.EndTime; + const progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0; + progressPercent = Math.min(100, Math.max(0, progressPercent)); + progressBarFill.style.width = `${progressPercent}%`; + progressBarFill.style.setProperty('--accent-color', mediaProps.AccentColor); + } + } + else + { + SetVisibility(false); + } +} + + + +////////////////////// +// HELPER FUNCTIONS // +////////////////////// + +function SetVisibility(visible) { + // Always clear any pending hide timers whenever we change visibility + if (hideTimeout) { + clearTimeout(hideTimeout); + hideTimeout = null; + } + + if (visible) { + standardLayout.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 { + standardLayout.style.animation = `${hideAnimation} 0.5s ease-out forwards`; + } +} + +async function ChangeTrack(mediaProps) { + // Fade in the overlay (shows the new text) + trackLabel.style.opacity = "0"; + 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(() => { + trackLabel.innerText = mediaProps.Title; + artistLabel.innerText = mediaProps.Artist; + + // Extract the image source string (use fallback if Windows has no art) + const newArtUrl = mediaProps.Base64Image; + const accent = mediaProps.AccentColor || "#ffffff"; + + // Set the image + backgroundLayer.style.backgroundImage = `url('${newArtUrl}')`; + albumArtLayer.style.backgroundImage = `url('${newArtUrl}')`; + + // Apply a tint: Use 30% opacity of the accent color + a dark overlay for contrast + // 'rgba(0,0,0,0.6)' ensures the text stays readable + backgroundLayer.style.backgroundColor = accent + "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: Use 30% opacity of the accent color + a dark overlay for contrast + // 'rgba(0,0,0,0.6)' ensures the text stays readable + backgroundTransitionLayer.style.backgroundColor = accent + "80"; // 80 is 50% opacity in hex + + SetVisibility(true); // Show the overlay for a few seconds if autoHide is enabled + }, 250); + }, 250); +} + +function SetAlbumArtSize() { + const height = songInfoContainer.offsetHeight; + albumArtContainer.style.height = `${height}px`; + albumArtContainer.style.width = `${height}px`; +} + +SetAlbumArtSize(); \ No newline at end of file diff --git a/now-playing/themes/standard/style.css b/now-playing/themes/standard/style.css new file mode 100644 index 0000000..891080b --- /dev/null +++ b/now-playing/themes/standard/style.css @@ -0,0 +1,304 @@ +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; /* Full viewport height */ +} + +#standard-layout { + width: 500px; + display: flex; + flex-direction: row; + /* This forces both containers to be the same height */ + align-items: stretch; + gap: 0.5em; + opacity: 0; +} + +#album-art-container { + /* 1. Tell it to maintain a 1:1 square ratio */ + aspect-ratio: 1 / 1; + overflow: hidden; + border-radius: 1em; + position: relative; + background-size: cover; + background-position: center; +} + +#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; +} + +#art-layer { + z-index: 1; + transition: all 0.5s ease; +} + +#art-transition-layer { + z-index: 0; +} + +#song-info-container { + position: relative; + flex: 1; + display: flex; + flex-direction: column; + border-radius: 1em; + 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(0px) 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: 1em 1.75em; + display: flex; + flex-direction: column; + gap: 0.3em; +} + +#track-label { + font-size: 1em; + font-weight: 600; +} + +#artist-label { + font-size: 0.8em; + font-weight: 300; + opacity: 0.6; +} + +.text-label { + white-space: nowrap; + overflow: hidden; + mask-image: linear-gradient(to right, black calc(100% - 1em), transparent 100%); + -webkit-mask-image: linear-gradient(to right, black calc(100% - 1em), transparent 100%); + 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: 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-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; +} + +@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; + } +} \ No newline at end of file