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 &&