diff --git a/.common/resources/smtc-bridge-icon.png b/.common/resources/smtc-bridge-icon.png new file mode 100644 index 0000000..a17927d Binary files /dev/null and b/.common/resources/smtc-bridge-icon.png differ diff --git a/.common/utils/helpers.js b/.common/utils/helpers.js index 3e2816e..d0f1789 100644 --- a/.common/utils/helpers.js +++ b/.common/utils/helpers.js @@ -484,4 +484,242 @@ async function GetAccentPalette(imageUrl) { resolve(hexPalette); }); }); +} + +// Generic popup +function showPopup(iconSrc, title, subtitle, attribute, background, button) { + // 1. Check if a popup is already on screen + const existingOverlay = document.getElementById('global-common-overlay'); + + // If it exists AND isn't already in the middle of fading out, DO NOTHING + if (existingOverlay && !existingOverlay.classList.contains('common-closing')) { + return null; + } + + // If there IS a popup but it IS fading out, kill it instantly to make room for the new one + if (existingOverlay) { + existingOverlay.remove(); + } + + // 2. Inject global styles if they don't already exist in the head + if (!document.getElementById('global-common-popup-styles')) { + const styleSheet = document.createElement('style'); + styleSheet.id = 'global-common-popup-styles'; + styleSheet.innerText = ` + #global-common-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: transparent; + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + display: flex; + align-items: center; + justify-content: center; + z-index: 999999; + animation: commonFadeIn 2s cubic-bezier(0.16, 1, 0.3, 1) forwards; + } + .common-popup-card { + width: 90%; + max-width: 500px; + border-radius: 24px; + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: stretch; + overflow: hidden; + box-shadow: 0 30px 60px rgba(0,0,0,0.4), + 0 0 0 1px rgba(255, 255, 255, 0.08); + animation: commonScaleIn 2.5s cubic-bezier(0.16, 1, 0.3, 1) forwards; + gap: 32px; + padding: 32px; + } + .common-popup-icon-container { + display: flex; + align-items: center; + justify-content: center; + background: transparent; + } + .common-popup-img { + height: 48px; + width: auto; + object-fit: contain; + display: block; + } + .common-popup-text-content { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + text-align: left; + gap: 8px; + } + .common-popup-title { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 22px; + font-weight: 700; + color: #ffffff; + letter-spacing: -0.02em; + line-height: 1.2; + } + .common-popup-title:last-child { + margin-bottom: 0; + } + .common-popup-subtitle { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + font-weight: 400; + color: rgba(255, 255, 255, 0.65); + margin: 0; + line-height: 1.5; + } + .common-popup-attribute { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 13px; + color: rgba(255, 255, 255, 0.4); + word-break: break-all; + } + /* 💎 GLASSMORPHIC BUTTON STYLES */ + .common-popup-button { + margin-top: 6px; + padding: 10px 20px; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.1); + color: #ffffff; + font-family: system-ui, sans-serif; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s ease, border-color 0.2s ease, transform 0.1s ease; + text-align: center; + width: fit-content; + } + .common-popup-button:hover { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.3); + } + .common-popup-button:active { + transform: scale(0.97); + } + @keyframes commonFadeIn { + from { opacity: 0; } to { opacity: 1; } + } + @keyframes commonScaleIn { + from { transform: scale(0.95) translateY(10px); opacity: 0; } + to { transform: scale(1) translateY(0); opacity: 1; } + } + #global-common-overlay.common-closing { + animation: commonFadeOut 2s cubic-bezier(0.16, 1, 0.3, 1) forwards; + } + .common-closing .common-popup-card { + animation: commonScaleOut 2.5s cubic-bezier(0.16, 1, 0.3, 1) forwards; + } + @keyframes commonFadeOut { + from { opacity: 1; } to { opacity: 0; } + } + @keyframes commonScaleOut { + from { transform: scale(1) translateY(0); opacity: 1; } + to { transform: scale(0.95) translateY(10px); opacity: 0; } + } + `; + document.head.appendChild(styleSheet); + } + + // 3. Create elements + const overlay = document.createElement('div'); + overlay.id = 'global-common-overlay'; + + const card = document.createElement('div'); + card.className = 'common-popup-card'; + card.style.background = background || 'linear-gradient(135deg, #1a1a1e 0%, #111113 100%)'; + + // 4. Construct Inner Content (Strict Conditional Rendering) + const iconMarkup = iconSrc + ? `
+ Alert Icon +
` + : ''; + + const titleMarkup = title + ? `` + : ''; + + const subtitleMarkup = subtitle + ? `` + : ''; + + const attributeMarkup = attribute + ? `` + : ''; + + // Render button conditional markup + const buttonMarkup = (button && button.text) + ? `` + : ''; + + card.innerHTML = ` + ${iconMarkup} +
+ ${titleMarkup} + ${subtitleMarkup} + ${attributeMarkup} + ${buttonMarkup} +
+ `; + + // 5. Setup Action Listeners if Button Exists + if (button && button.text && button.action) { + const actionBtn = card.querySelector('.common-popup-button'); + if (actionBtn) { + actionBtn.addEventListener('click', () => { + if (typeof button.action === 'function') { + button.action(); + } else if (typeof button.action === 'string') { + window.open(button.action, '_blank'); + } + }); + } + } + + // 6. Append to page DOM + overlay.appendChild(card); + document.body.appendChild(overlay); + + return { + element: overlay, + close: () => { + overlay.classList.add('common-closing'); + overlay.addEventListener('animationend', (event) => { + if (event.target === overlay) { + overlay.remove(); + } + }); + } + }; +} + +function VersionCheck(requiredVersion, installedVersion) { + const [rMajor, rMinor, rPatch] = requiredVersion.split('.').map(Number); + const [iMajor, iMinor, iPatch] = installedVersion.split('.').map(Number); + + // 1. CRITICAL: Major versions must match exactly. + if (iMajor !== rMajor) { + return 'incompatible'; + } + + // 2. SOFT WARNING: The installed minor version is lower than what is required. + if (iMinor < rMinor) { + return 'soft-warning'; + } + + // 3. SOFT WARNING: Minors match, but the installed patch version is lagging. + if (iMinor === rMinor && iPatch < rPatch) { + return 'soft-warning'; + } + + // 4. PERFECT: Installed version meets or exceeds the required baseline. + return 'compatible'; } \ No newline at end of file diff --git a/now-playing/script.js b/now-playing/script.js index bf57c3b..4c20b44 100644 --- a/now-playing/script.js +++ b/now-playing/script.js @@ -37,6 +37,11 @@ const hideAnimation = urlParams.get('hideAnimation') || 'slide-out-bottom'; // GLOBAL VARS // ///////////////// +const REQUIRED_VERSION = '0.0.1'; +const SMTC_BRIDGE_DOWNLOAD_URL = 'https://github.com/nuttylmao/smtc-bridge/releases'; +let smtcBridgePopup = null; +let versionCheckPopup = null; +let skipVersionCheck = false; let CurrentPlaybackStatus; let CurrentSong; let hideTimeout = null; @@ -141,20 +146,49 @@ async function FetchMedia() { const response = await fetch('http://localhost:5000/now-playing'); const data = await response.json(); + // Remove the SMTC Bridge popup if it's on screen + if (smtcBridgePopup) { + smtcBridgePopup.close(); + smtcBridgePopup = null; + } + + // Check the SMTC Bridge version + CheckSMTCBridgeVersion(data.app_version); + // 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 + const newPopup = showPopup( + '/.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; + } } } -// Start polling every 1000 milliseconds -setInterval(FetchMedia, 1000); +window.addEventListener('DOMContentLoaded', () => { + // Start polling every 1000 milliseconds + setInterval(FetchMedia, 1000); -// Run once immediately on script load -FetchMedia(); + // Run once immediately on script load + FetchMedia(); +}); @@ -162,6 +196,77 @@ FetchMedia(); // 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': + { + const newPopup = showPopup( + '/.common/resources/smtc-bridge-icon.png', + 'Update Required', + 'Your version of SMTC Bridge is out of date.', + `Installed Version: ${installedVersion}
New Version: ${REQUIRED_VERSION}`, + 'linear-gradient(0deg, #1b0005 0%, #4f000d 100%)', + { + text: 'Download', + action: () => { + window.open(SMTC_BRIDGE_DOWNLOAD_URL, "_blank"); + } + } + ); + + if (newPopup) { + versionCheckPopup = newPopup; + } + } + + skipVersionCheck = false; + break; + case 'soft-warning': + { + const newPopup = showPopup( + '/.common/resources/smtc-bridge-icon.png', + 'Update Available', + 'A new version of SMTC Bridge is available.', + `Installed Version: ${installedVersion}
New Version: ${REQUIRED_VERSION}`, + 'linear-gradient(0deg, #2a004f 0%, #4f2675 100%)', + { + text: 'Download', + action: () => { + window.open(SMTC_BRIDGE_DOWNLOAD_URL, "_blank"); + } + } + ); + + if (newPopup) { + versionCheckPopup = newPopup; + } + + setTimeout(() => { + if (versionCheckPopup) { + versionCheckPopup.close(); + versionCheckPopup = null; + } + }, 10000); + } + skipVersionCheck = true; + break; + default: + if (versionCheckPopup) { + versionCheckPopup.close(); + versionCheckPopup = null; + } + break; + } +} + function ConvertMillisecondsToMinutesSoThatItLooksBetterOnTheOverlay(time) { if (isNaN(time) || time <= 0) return "0:00";