mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-12 05:10:52 +02:00
add backdrop blur to now playing screen
This commit is contained in:
@@ -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() {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<NowPlayingWash
|
||||
artworkUri={washArtworkUri}
|
||||
offset={{
|
||||
top: -(insets.top + CONTENT_TOP_PADDING),
|
||||
left: -(insets.left + layout.contentPadding),
|
||||
right: -(insets.right + layout.contentPadding),
|
||||
}}
|
||||
/>
|
||||
<View style={[styles.shell, { width: layout.contentWidth }]}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerSide}>
|
||||
|
||||
@@ -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 (
|
||||
<View
|
||||
pointerEvents="none"
|
||||
style={{ position: 'absolute', top: offset.top, left: offset.left, right: offset.right, height: bandH }}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: artworkUri }}
|
||||
style={[StyleSheet.absoluteFill, { opacity: ART_OPACITY }]}
|
||||
contentFit="cover"
|
||||
blurRadius={BLUR_RADIUS}
|
||||
transition={null}
|
||||
/>
|
||||
<Canvas style={StyleSheet.absoluteFill}>
|
||||
<Rect x={0} y={0} width={bandW} height={bandH}>
|
||||
<LinearGradient
|
||||
start={vec(0, 0)}
|
||||
end={vec(0, bandH)}
|
||||
colors={[`${colors.bgPrimary}00`, `${colors.bgPrimary}00`, colors.bgPrimary]}
|
||||
positions={[0, FADE_START, 1]}
|
||||
/>
|
||||
</Rect>
|
||||
</Canvas>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -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/<hash>` 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'
|
||||
|
||||
Reference in New Issue
Block a user