From af33698b6918cf618fba88bdeb76e9581d3db31d Mon Sep 17 00:00:00 2001 From: Boof2015 <75185879+Boof2015@users.noreply.github.com> Date: Wed, 8 Jul 2026 01:47:19 -0400 Subject: [PATCH] add backdrop blur to now playing screen --- src/app/now-playing.tsx | 16 ++++++++ src/components/NowPlayingWash.tsx | 65 +++++++++++++++++++++++++++++++ src/library/artwork.ts | 20 ++++++++++ 3 files changed, 101 insertions(+) create mode 100644 src/components/NowPlayingWash.tsx diff --git a/src/app/now-playing.tsx b/src/app/now-playing.tsx index 74f66b1..63a556e 100644 --- a/src/app/now-playing.tsx +++ b/src/app/now-playing.tsx @@ -23,6 +23,7 @@ import { AstraLogo } from '@/components/AstraLogo'; import { FormatBadges } from '@/components/FormatBadge'; import { RemoteSourceBadge } from '@/components/RemoteSourceBadge'; import { MarqueeText } from '@/components/MarqueeText'; +import { NowPlayingWash } from '@/components/NowPlayingWash'; import { SeekBar } from '@/components/SeekBar'; import { WaveformSeekBar } from '@/components/WaveformSeekBar'; import { Visualizer } from '@/components/Visualizer'; @@ -38,6 +39,7 @@ import { createThemedStyles, useColors } from '@/theme/themed'; import { WIDE_MIN_WIDTH, isWideWindow } from '@/theme/adaptive'; import { motion } from '@/theme/motion'; import { resolveNavigationArtist } from '@/library/artistGrouping'; +import { artworkThumbFromSource } from '@/library/artwork'; import { useLibraryStore } from '@/stores/libraryStore'; import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore'; import { usePlayerStore } from '@/stores/playerStore'; @@ -314,6 +316,12 @@ export default function NowPlayingScreen() { const activeTrack = desktopSnapshot?.currentTrack ?? null; const isPlaying = activePresentation.playbackState === 'playing'; const isLoading = activePresentation.playbackState === 'loading'; + // Wash off a low-res thumbnail (like the album/artist detail headers do) so the + // blur reads as pure colors — full-res art keeps its detail at any blur radius. + // currentTrack only carries the full-size artworkData, so derive the thumb from it. + const washArtworkUri = artworkThumbFromSource( + isDesktopTarget ? activePresentation.artworkUri : track?.artworkData ?? null + ); const availableHeight = windowHeight - insets.top - insets.bottom; const effectiveWidth = windowWidth - insets.left - insets.right; const layout = getNowPlayingLayout( @@ -496,6 +504,14 @@ export default function NowPlayingScreen() { }, ]} > + diff --git a/src/components/NowPlayingWash.tsx b/src/components/NowPlayingWash.tsx new file mode 100644 index 0000000..06f3699 --- /dev/null +++ b/src/components/NowPlayingWash.tsx @@ -0,0 +1,65 @@ +import { StyleSheet, View, useWindowDimensions } from 'react-native'; +import { Image } from 'expo-image'; +import { + Canvas, + LinearGradient, + Rect, + vec +} from '@shopify/react-native-skia'; +import { useColors } from '@/theme/themed'; + +// A faint wash of the current cover art bleeding from the very top of the +// now-playing sheet, fading back to the background before it reaches the +// artwork. Reuses the detail-header wash idiom (blurred art + Skia fade) so it +// reads as native to Astra rather than a generic full-bleed player background. +// Tune these on device — the mockup was "too bright", so start subtle. +/** Fraction of the screen height the wash spans before it's fully gone. */ +const REACH = 0.42; +/** Base strength of the blurred art at the very top edge. */ +const ART_OPACITY = 0.3; +/** Where the fade-to-background begins within the band (0 = top, 1 = bottom). */ +const FADE_START = 0.45; +/** Matches the album/artist detail wash — blurring a low-res thumb reads as pure color. */ +const BLUR_RADIUS = 40; + +export function NowPlayingWash({ + artworkUri, + offset, +}: { + artworkUri: string | null; + /** Negative insets so the band bleeds past the padded `content` node to the true screen edges. */ + offset: { top: number; left: number; right: number }; +}) { + const colors = useColors(); + const { width, height } = useWindowDimensions(); + if (!artworkUri) return null; + const bandH = Math.round(height * REACH); + // The band bleeds past the padded parent on both sides (offsets are negative), + // so the fade Rect must span the full band width — not just the screen — or the + // uncovered edge strips show the blurred image's raw cutoff. + const bandW = Math.round(width - offset.left - offset.right); + return ( + + + + + + + + + ); +} diff --git a/src/library/artwork.ts b/src/library/artwork.ts index 8027d31..ce77483 100644 --- a/src/library/artwork.ts +++ b/src/library/artwork.ts @@ -47,6 +47,26 @@ export function artworkThumbUri(hash: string): string { return `file://${getArtworkThumbDir()}/${artworkThumbFileName(hash)}`; } +/** + * Low-res thumbnail for a live artwork source. The player store's currentTrack + * (rebuilt from RNTP via `rntpToTrack`) only carries the full-size `artworkData` + * — `file://…/artwork/` for local tracks — not the hash. Recover the cached + * file name from the path and point at the generated thumb so callers can blur it + * down to pure color. Remote URLs and base64 data URLs have no local thumb, so + * they pass through unchanged. + */ +export function artworkThumbFromSource(source: string | null | undefined): string | null { + if (!source) return null; + if (!source.startsWith('file://')) return source; + const name = source.split('/').pop(); + if (!name) return source; + try { + return artworkThumbUri(decodeURIComponent(name)); + } catch { + return artworkThumbUri(name); + } +} + type TrackArtworkFields = Pick< DbTrack, 'source_type' | 'source_id' | 'artwork_source_id' | 'artwork_hash'