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'