From 4148e063dd938c370fc08f711717493d88f32a3b Mon Sep 17 00:00:00 2001 From: cucadmuh <49571317+cucadmuh@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:49:59 +0300 Subject: [PATCH] fix(home): sync Mainstage hero backdrop with album on fast nav (#1021) * fix(home): sync Mainstage hero backdrop with album on fast nav Bind hero background URL to the current slide and drop stale crossfade layers so rapid carousel clicks no longer show the previous album's art. * docs(changelog): note Mainstage hero backdrop sync fix (PR #1021) --- CHANGELOG.md | 8 ++++++++ src/components/Hero.tsx | 36 +++++++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2f45eea..8aaa3e0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -136,6 +136,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +### Mainstage — hero backdrop stays in sync when skipping albums quickly + +**By [@cucadmuh](https://github.com/cucadmuh), reported by Asra on the Psysonic Discord, PR [#1021](https://github.com/Psychotoxical/psysonic/pull/1021)** + +* Rapid prev/next clicks on the Mainstage hero no longer leave the blurred cover-art background on the previous album while the foreground cover and metadata already show the next one. + + + ## [1.47.0] > **🙏 Thank you to our amazing Discord community.** This release would not have been possible without your tireless support, quality checks, bug reports and all-round collaboration. Every report, every repro and every bit of feedback shaped what shipped here — thank you. Come join us: [discord.gg/AMnDRErm4u](https://discord.gg/AMnDRErm4u) diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index 669df452..7fd371c3 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -34,14 +34,25 @@ function HeroBg({ url }: { url: string }) { const [layers, setLayers] = useState>(() => url ? [{ url, id: 0, visible: true }] : [] ); - const counter = useRef(1); + const counter = useRef(url ? 1 : 0); + const latestUrlRef = useRef(url); + latestUrlRef.current = url; useEffect(() => { - if (!url) return; + if (!url) { + setLayers([]); + return; + } const id = counter.current++; setLayers(prev => [...prev, { url, id, visible: false }]); - const t1 = setTimeout(() => setLayers(prev => prev.map(l => ({ ...l, visible: l.id === id }))), 20); - const t2 = setTimeout(() => setLayers(prev => prev.filter(l => l.id === id)), 900); + const t1 = setTimeout(() => { + if (latestUrlRef.current !== url) return; + setLayers(prev => prev.map(l => ({ ...l, visible: l.id === id }))); + }, 20); + const t2 = setTimeout(() => { + if (latestUrlRef.current !== url) return; + setLayers(prev => prev.filter(l => l.id === id)); + }, 900); return () => { clearTimeout(t1); clearTimeout(t2); }; }, [url]); @@ -280,17 +291,20 @@ export default function Hero({ albums: albumsProp }: HeroProps = {}) { ensurePriority: 'high', }); - // Keep the last known good URL so HeroBg never receives '' during a cache-miss - // transition (which would cause the background to flash empty before fading in). - const stableBgUrl = useRef(''); + // Per-album fallback so a cache miss on the current slide does not flash empty, + // but never reuse another album's art (that caused bg/foreground desync on fast nav). + const stableBgByAlbum = useRef>({}); const albumId = album?.id; + useEffect(() => { + if (bgHandle.src && albumId) { + stableBgByAlbum.current[albumId] = bgHandle.src; + } + }, [bgHandle.src, albumId]); + const heroBgUrl = bgHandle.src || (albumId ? stableBgByAlbum.current[albumId] ?? '' : ''); const { isHolding, pressBind } = useLongPressAction({ onShortPress: () => { if (albumId) playAlbum(albumId); }, onLongPress: () => { if (albumId) playAlbumShuffled(albumId); }, }); - useEffect(() => { - if (bgHandle.src) stableBgUrl.current = bgHandle.src; - }, [bgHandle.src, albumId]); if (!album) return
; @@ -303,7 +317,7 @@ export default function Hero({ albums: albumsProp }: HeroProps = {}) { onClick={() => navigateToAlbum(album.id)} style={{ cursor: 'pointer' }} > - {enableCoverArtBackground && !perfFlags.disableMainstageHeroBackdrop && heroInView && } + {enableCoverArtBackground && !perfFlags.disableMainstageHeroBackdrop && heroInView && } {enableCoverArtBackground && !perfFlags.disableMainstageHeroBackdrop && heroInView &&