From b20e942b4dfcd78aff2ccb258632cf901d343d0f Mon Sep 17 00:00:00 2001 From: meowarex Date: Thu, 13 Aug 2026 21:44:37 +1000 Subject: [PATCH 1/3] Cover Everywhere (Stock Support) --- .../src/backdrop-styles.css | 4 +- .../src/cover-everywhere.css | 99 +++++++++++++++++++ plugins/radiant-lyrics-luna/src/index.ts | 88 +++++++++++++++++ 3 files changed, 189 insertions(+), 2 deletions(-) diff --git a/plugins/radiant-lyrics-luna/src/backdrop-styles.css b/plugins/radiant-lyrics-luna/src/backdrop-styles.css index 86fa12d..097839d 100644 --- a/plugins/radiant-lyrics-luna/src/backdrop-styles.css +++ b/plugins/radiant-lyrics-luna/src/backdrop-styles.css @@ -15,13 +15,13 @@ } /* Hide Tidal now-playing background color */ -[data-test="new-now-playing"] > [class*="_background_"] { +body.rl-backdrop-active [data-test="new-now-playing"] > [class*="_background_"] { /* biome-ignore lint: Must override native album-art-derived background */ display: none !important; } /* Ensure now-playing container is transparent */ -[class*="_nowPlayingContainer"] { +body.rl-backdrop-active [class*="_nowPlayingContainer"] { /* biome-ignore lint: Must override any inline background styles */ background: transparent !important; } diff --git a/plugins/radiant-lyrics-luna/src/cover-everywhere.css b/plugins/radiant-lyrics-luna/src/cover-everywhere.css index c5da420..9005488 100644 --- a/plugins/radiant-lyrics-luna/src/cover-everywhere.css +++ b/plugins/radiant-lyrics-luna/src/cover-everywhere.css @@ -1,6 +1,105 @@ /* Cover-art backdrop container styles */ +/* Stock backdrop projected app wide */ +.rl-stock-background-container { + position: fixed; + left: 0; + top: 0; + width: 100vw; + height: 100vh; + z-index: -3; + pointer-events: none; + overflow: hidden; + background-color: #000; +} + +.rl-stock-rotator { + position: absolute; + left: 50%; + top: 50%; + width: 100vmax; + height: 100vmax; + margin-left: -50vmax; + margin-top: -50vmax; + will-change: transform; + animation: rl-stock-spin 33s linear infinite; +} + +/* Tidal lays 40% of the container & then scales it 4x which is actually kinda smart (i should have thought of this..)*/ +.rl-stock-overscan { + position: absolute; + left: 50%; + top: 50%; + width: 40vmax; + height: 40vmax; + margin-left: -20vmax; + margin-top: -20vmax; + transform: scale(4); +} + +.rl-stock-art { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + object-fit: cover; + filter: blur(28px) saturate(1.3); + transform: scale(1.26); + opacity: 0; + /* Crossfade between the two layers on track change (again.. NOT AUDIO CROSSFADE) */ + transition: opacity 600ms ease; +} + +.rl-stock-art.rl-stock-art-active { + opacity: 1; +} + +.rl-stock-scrim { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + /* Both Tidal gradients */ + background: + radial-gradient( + 120% 100% at 50% 38%, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 0.12) 62%, + rgba(0, 0, 0, 0.28) 100% + ), + linear-gradient( + rgba(0, 0, 0, 0.34) 0%, + rgba(0, 0, 0, 0.42) 24%, + rgba(0, 0, 0, 0.5) 45%, + rgba(0, 0, 0, 0.62) 72%, + rgba(0, 0, 0, 0.76) 100% + ); +} + +/* Tidal stops its spin while paused */ +.rl-stock-paused .rl-stock-rotator { + animation-play-state: paused; +} + +@keyframes rl-stock-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: reduce) { + .rl-stock-rotator { + /* biome-ignore lint: Accessibility override needs priority */ + animation: none !important; + } +} + .global-background-container { position: fixed; left: 0; diff --git a/plugins/radiant-lyrics-luna/src/index.ts b/plugins/radiant-lyrics-luna/src/index.ts index 22f9401..fe5276c 100644 --- a/plugins/radiant-lyrics-luna/src/index.ts +++ b/plugins/radiant-lyrics-luna/src/index.ts @@ -727,6 +727,9 @@ const setLayerRunning = ( }; const syncBackdropActivity = (): void => { + // Stock backdrop keeps current (no shader loop) + if (stockContainer) applyStockCoverEverywhere(); + const nowPlaying = kawarpLayers.nowPlaying; const global = kawarpLayers.global; const nowPlayingVisible = nowPlaying?.isVisible() ?? false; @@ -858,10 +861,15 @@ function updateCoverArtBackground(method: number = 0): void { } // Teardown <3 + document.body.classList.toggle("rl-backdrop-active", settings.backdropEnabled); if (!settings.backdropEnabled) { cleanUpDynamicArt(); + // Cover Everywhere still applies (Stock Tidal Backdrop) + if (settings.CoverEverywhere) applyStockCoverEverywhere(); + else cleanUpStockCoverEverywhere(); return; } + cleanUpStockCoverEverywhere(); const coverArtImageSrc = getCoverArtSrc(); if (!coverArtImageSrc) { @@ -875,6 +883,77 @@ function updateCoverArtBackground(method: number = 0): void { syncBackdropActivity(); } +// MARKER: Stock Backdrop (Cover Everywhere without the shader) + +const STOCK_MARKUP = + '
' + + '' + + '
'; +const STOCK_ART_RES = "320x320"; + +let stockBgStyleTag: StyleTag | null = null; +let stockContainer: HTMLElement | null = null; + +const applyStockCoverEverywhere = (): void => { + // Same sheet the shader uses + stockBgStyleTag ??= new StyleTag( + "RadiantLyrics-stock-backdrop", + unloads, + coverEverywhereCss, + ); + + if (!stockContainer?.isConnected) { + // Sweep first + cleanUpStockContainers(); + stockContainer = document.createElement("div"); + stockContainer.className = "rl-stock-background-container"; + stockContainer.innerHTML = STOCK_MARKUP; + document.body.appendChild(stockContainer); + } + + const source = document.querySelector( + '[data-test="footer-player"] [data-test="current-media-imagery"] img', + ); + const layers = [ + ...stockContainer.querySelectorAll(".rl-stock-art"), + ]; + // Which layer is showing lives in DOM (no state to track) + const shown = + layers.find((l) => l.classList.contains("rl-stock-art-active")) ?? layers[1]; + const next = layers.find((l) => l !== shown); + + const cover = source?.src.replace(/\d+x\d+/, STOCK_ART_RES); + if (next && cover && cover !== shown.src) { + next.src = cover; + // Swap only once decoded (otherwise the crossfade reveals a blank layer) + const reveal = () => { + next.classList.add("rl-stock-art-active"); + shown.classList.remove("rl-stock-art-active"); + }; + if (next.complete) reveal(); + else next.addEventListener("load", reveal, { once: true }); + } + + // Tidal halts its own spin while paused + stockContainer.classList.toggle( + "rl-stock-paused", + !reduxPlaybackIsPlaying(), + ); +}; + +const cleanUpStockContainers = (): void => { + document + .querySelectorAll(".rl-stock-background-container") + .forEach((el) => el.remove()); +}; + +const cleanUpStockCoverEverywhere = (): void => { + stockContainer = null; + cleanUpStockContainers(); + stockBgStyleTag?.remove(); + stockBgStyleTag = null; +}; + const cleanUpGlobalBackground = function (): void { // Release WebGL context before canvas is orphaned (otherwise death occurs) disposeKawarpLayer("global"); @@ -912,10 +991,17 @@ const updateRadiantLyricsBackdrop = function (): void { document.body.classList.remove("performance-mode"); } + // Gates the rules that hide Tidal now-playing background + document.body.classList.toggle("rl-backdrop-active", settings.backdropEnabled); + if (!settings.backdropEnabled) { cleanUpDynamicArt(); + // Cover Everywhere still applies (Stock Tidal Backdrop) + if (settings.CoverEverywhere) applyStockCoverEverywhere(); + else cleanUpStockCoverEverywhere(); return; } + cleanUpStockCoverEverywhere(); if (!settings.CoverEverywhere) cleanUpGlobalBackground(); @@ -965,6 +1051,8 @@ updateCoverArtBackground(1); // Cleanups unloads.add(() => { cleanUpDynamicArt(); + cleanUpStockCoverEverywhere(); + document.body.classList.remove("rl-backdrop-active"); // Clean up floating player bar inline styles const footerPlayer = document.querySelector( From c369de4f1b8b89d28900dddebc4e13026b5ef1cd Mon Sep 17 00:00:00 2001 From: meowarex Date: Thu, 13 Aug 2026 22:07:09 +1000 Subject: [PATCH 2/3] Center Cover Art <3 --- plugins/radiant-lyrics-luna/src/Settings.tsx | 329 +++++++++---------- plugins/radiant-lyrics-luna/src/index.ts | 199 ++++------- plugins/radiant-lyrics-luna/src/styles.css | 45 +-- 3 files changed, 239 insertions(+), 334 deletions(-) diff --git a/plugins/radiant-lyrics-luna/src/Settings.tsx b/plugins/radiant-lyrics-luna/src/Settings.tsx index 9f69b08..17d3abd 100644 --- a/plugins/radiant-lyrics-luna/src/Settings.tsx +++ b/plugins/radiant-lyrics-luna/src/Settings.tsx @@ -6,8 +6,6 @@ declare global { interface Window { updateRadiantLyricsStyles?: () => void; updateRadiantLyricsTextGlow?: () => void; - updateStickyLyricsFeature?: () => void; - updateStickyLyricsSetting?: (checked: boolean) => void; updateRadiantLyricsPlayerBarTint?: () => void; updateRadiantLyricsBackdrop?: () => void; updateQualityProgressColor?: () => void; @@ -31,7 +29,6 @@ export const settings = await ReactiveStore.getPluginStorage("RadiantLyrics", { bubbledLyrics: true, romanizeLyrics: false, aiSyllables: false, - stickyLyrics: false, syllableStyle: 0, // MARKER: Syllable animations SETTINGS (WIP coming soon) syllableLogging: false, hideUIEnabled: true, @@ -153,14 +150,6 @@ export const Settings = () => { const [tintHoveredColorIndex, setTintHoveredColorIndex] = React.useState< number | null >(null); - const [stickyLyrics, setStickyLyrics] = React.useState(settings.stickyLyrics); - React.useEffect(() => { - window.updateStickyLyricsSetting = (checked: boolean) => - setStickyLyrics(checked); - return () => { - window.updateStickyLyricsSetting = undefined; - }; - }, []); const [lyricsStyle, setLyricsStyle] = React.useState(settings.lyricsStyle); React.useEffect(() => { window.updateLyricsStyleSetting = (value: number) => setLyricsStyle(value); @@ -317,19 +306,7 @@ export const Settings = () => { } }} /> - { - settings.stickyLyrics = checked; - setStickyLyrics(checked); - if (window.updateStickyLyricsFeature) { - window.updateStickyLyricsFeature(); - } - }} - /> - { refreshBackdrop(); }} /> - { - settings.backdropStyle = value; - setBackdropStyle(value); - refreshBackdrop(); - }} - /> - { - settings.backdropPlaybackReactive = checked; - setBackdropPlaybackReactive(checked); - refreshBackdrop(); - }} - /> { refreshBackdrop(); }} /> - { - settings.performanceMode = checked; - setPerformanceMode(checked); - refreshBackdrop(); - }} - /> - { - settings.backdropOpacity = value; - setBackdropOpacity(value); - refreshBackdrop(); - }} - /> - { - settings.backdropWarp = value; - setBackdropWarp(value); - refreshBackdrop(); - }} - /> - { - settings.backdropBlurPasses = value; - setBackdropBlurPasses(value); - refreshBackdrop(); - }} - /> - { - settings.backdropSpeed = value; - setBackdropSpeed(value); - refreshBackdrop(); - }} - /> - { - settings.backdropContrast = value; - setBackdropContrast(value); - refreshBackdrop(); - }} - /> - { - settings.backdropDarken = value; - setBackdropDarken(value); - refreshBackdrop(); - }} - /> - { - settings.backdropSaturation = value; - setBackdropSaturation(value); - refreshBackdrop(); - }} - /> - { - settings.backdropScale = value; - setBackdropScale(value); - refreshBackdrop(); - }} - /> - { - settings.backdropDithering = value; - setBackdropDithering(value); - refreshBackdrop(); - }} - /> + {backdropEnabled && ( + <> + { + settings.backdropStyle = value; + setBackdropStyle(value); + refreshBackdrop(); + }} + /> + { + settings.backdropPlaybackReactive = checked; + setBackdropPlaybackReactive(checked); + refreshBackdrop(); + }} + /> + { + settings.performanceMode = checked; + setPerformanceMode(checked); + refreshBackdrop(); + }} + /> + { + settings.backdropOpacity = value; + setBackdropOpacity(value); + refreshBackdrop(); + }} + /> + { + settings.backdropWarp = value; + setBackdropWarp(value); + refreshBackdrop(); + }} + /> + { + settings.backdropBlurPasses = value; + setBackdropBlurPasses(value); + refreshBackdrop(); + }} + /> + { + settings.backdropSpeed = value; + setBackdropSpeed(value); + refreshBackdrop(); + }} + /> + { + settings.backdropContrast = value; + setBackdropContrast(value); + refreshBackdrop(); + }} + /> + { + settings.backdropDarken = value; + setBackdropDarken(value); + refreshBackdrop(); + }} + /> + { + settings.backdropSaturation = value; + setBackdropSaturation(value); + refreshBackdrop(); + }} + /> + { + settings.backdropScale = value; + setBackdropScale(value); + refreshBackdrop(); + }} + /> + { + settings.backdropDithering = value; + setBackdropDithering(value); + refreshBackdrop(); + }} + /> + + )} ); }; diff --git a/plugins/radiant-lyrics-luna/src/index.ts b/plugins/radiant-lyrics-luna/src/index.ts index fe5276c..2e5ab62 100644 --- a/plugins/radiant-lyrics-luna/src/index.ts +++ b/plugins/radiant-lyrics-luna/src/index.ts @@ -187,6 +187,11 @@ const applyPlayerBarTintToElement = (): void => { footerPlayer.style.removeProperty("left"); footerPlayer.style.removeProperty("width"); } + // Now Playing centres its artwork in a row running to the viewport bottom but the player bar overlays it so i do some complicaated thing to reserve the space <3 + document.documentElement.style.setProperty( + "--rl-player-bar-inset", + `${Math.round(footerPlayer.getBoundingClientRect().height) + (settings.floatingPlayerBar ? settings.playerBarSpacing : 0)}px`, + ); applyIntegratedSeekbarChrome(); }; @@ -1089,9 +1094,9 @@ unloads.add(() => { if (btn?.parentNode) btn.parentNode.removeChild(btn); } - // Clean up sticky lyrics elements + // Clean up lyrics menu elements document - .querySelectorAll(".sticky-lyrics-trigger, .sticky-lyrics-dropdown") + .querySelectorAll(".rl-lyrics-menu-trigger, .rl-lyrics-menu") .forEach((el) => { el.remove(); }); @@ -1100,17 +1105,17 @@ unloads.add(() => { cleanUpDynamicArt(); }); -// MARKER: Sticky Lyrics Feature +// MARKER: Lyrics Menu (dropdown on the lyrics toggle) -const STICKY_ICON = +const LYRICS_MENU_ICON = ''; -const applyStickyIcon = (): void => { +const applyLyricsMenuIcon = (): void => { const trigger = document.querySelector( - ".sticky-lyrics-trigger", + ".rl-lyrics-menu-trigger", ) as HTMLElement; if (!trigger) return; - trigger.innerHTML = STICKY_ICON; + trigger.innerHTML = LYRICS_MENU_ICON; trigger.style.paddingLeft = "5px"; }; @@ -1154,31 +1159,22 @@ const sylTrace = (...args: unknown[]) => { }, }; -// Called from Settings (mirrors dropdown checkbox) -const updateStickyLyricsFeature = (): void => { - const checkbox = document.querySelector( - 'input[data-setting="stickyLyrics"]', - ) as HTMLInputElement; - if (checkbox) checkbox.checked = settings.stickyLyrics; -}; -(window as any).updateStickyLyricsFeature = updateStickyLyricsFeature; - -let stickyDropdownEl: HTMLElement | null = null; -let stickyDropdownOpen = false; +let lyricsMenuEl: HTMLElement | null = null; +let lyricsMenuOpen = false; const positionDropdown = (): void => { - if (!stickyDropdownEl) return; + if (!lyricsMenuEl) return; const toggle = document.querySelector('[data-test="toggle-lyrics"]') as HTMLElement; if (!toggle) return; const rect = toggle.getBoundingClientRect(); - stickyDropdownEl.style.top = `${rect.bottom}px`; - stickyDropdownEl.style.left = `${rect.left}px`; - stickyDropdownEl.style.width = `${rect.width}px`; - stickyDropdownEl.style.display = "block"; + lyricsMenuEl.style.top = `${rect.bottom}px`; + lyricsMenuEl.style.left = `${rect.left}px`; + lyricsMenuEl.style.width = `${rect.width}px`; + lyricsMenuEl.style.display = "block"; }; -const openStickyDropdown = (toggle: HTMLElement): void => { - stickyDropdownOpen = true; +const openLyricsMenu = (toggle: HTMLElement): void => { + lyricsMenuOpen = true; document.body.classList.add("rl-dropdown-open"); let positioned = false; const onWidened = (e: TransitionEvent) => { @@ -1199,41 +1195,35 @@ const openStickyDropdown = (toggle: HTMLElement): void => { }, 200); }; -const closeStickyDropdown = (): void => { - stickyDropdownOpen = false; +const closeLyricsMenu = (): void => { + lyricsMenuOpen = false; document.body.classList.remove("rl-dropdown-open"); - if (stickyDropdownEl) stickyDropdownEl.style.display = "none"; + if (lyricsMenuEl) lyricsMenuEl.style.display = "none"; }; -const ensureStickyDropdown = (): HTMLElement => { - if (stickyDropdownEl) return stickyDropdownEl; +const ensureLyricsMenu = (): HTMLElement => { + if (lyricsMenuEl) return lyricsMenuEl; const dropdown = document.createElement("div"); - dropdown.className = "sticky-lyrics-dropdown"; + dropdown.className = "rl-lyrics-menu"; dropdown.style.display = "none"; dropdown.innerHTML = ` -
- Sticky Lyrics - -
- Romanization -