Inspired by a dashboard design the user shared. - App: new palette and fonts, borderless rounded panels with soft shadows, a pink-to-blue gradient card for your own song (text and controls switch to white on it), flat cards for friends, a 'Hi Chris, welcome back' header, an outline bell, and a pink pill on the active tab. - Website: the same look, plus a wide-screen layout with a sidebar, the page in a rounded panel, and a right column with your profile, recent activity and recent plays. Phones keep the top bar and bottom tabs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y2iUsKEWDuRGjK5FPmcUP
138 lines
5.6 KiB
TypeScript
138 lines
5.6 KiB
TypeScript
import { LinearGradient } from 'expo-linear-gradient';
|
|
import { router } from 'expo-router';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { Linking, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
import Animated, {
|
|
FadeIn,
|
|
FadeOut,
|
|
useAnimatedStyle,
|
|
useSharedValue,
|
|
withSequence,
|
|
withSpring,
|
|
} from 'react-native-reanimated';
|
|
import { SERVICE_LABELS, timeAgo, type NowPlayingDTO, type StreamingService } from '@musicshare/shared';
|
|
import { successHaptic, tapHaptic } from '@/lib/haptics';
|
|
import { OnHero, colors, fonts, heroGradient, panelShadow, radius, space } from '@/lib/theme';
|
|
import { Avatar } from './Avatar';
|
|
import { Equalizer } from './Equalizer';
|
|
import { Lyrics } from './Lyrics';
|
|
import { ProgressBar } from './ProgressBar';
|
|
import { Reactions } from './Reactions';
|
|
import { ServiceLinks, TrackRow } from './TrackCard';
|
|
|
|
export function NowPlayingCard({
|
|
np,
|
|
preferred,
|
|
showUser = true,
|
|
pinned,
|
|
onTogglePin,
|
|
}: {
|
|
np: NowPlayingDTO;
|
|
preferred: StreamingService | null | undefined;
|
|
showUser?: boolean;
|
|
pinned?: boolean;
|
|
onTogglePin?: () => void;
|
|
}) {
|
|
// Star "pop" when pinned.
|
|
const star = useSharedValue(1);
|
|
const wasPinned = useRef(pinned);
|
|
useEffect(() => {
|
|
if (pinned && !wasPinned.current) star.set(withSequence(withSpring(1.6, { damping: 6, stiffness: 400 }), withSpring(1)));
|
|
wasPinned.current = pinned;
|
|
}, [pinned, star]);
|
|
const starStyle = useAnimatedStyle(() => ({ transform: [{ scale: star.value }] }));
|
|
const [expanded, setExpanded] = useState(false);
|
|
const toggle = () => {
|
|
tapHaptic();
|
|
setExpanded((v) => !v);
|
|
};
|
|
|
|
const togglePin = () => {
|
|
if (pinned) tapHaptic();
|
|
else successHaptic();
|
|
onTogglePin?.();
|
|
};
|
|
|
|
// Your own song gets the gradient "hero" card; friends' songs sit on plain panels.
|
|
const hero = !showUser;
|
|
return (
|
|
<OnHero.Provider value={hero}>
|
|
<View style={[styles.card, hero && styles.heroCard, pinned && styles.pinned]}>
|
|
{hero && <LinearGradient colors={heroGradient} start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }} style={StyleSheet.absoluteFill} />}
|
|
|
|
<View style={styles.header}>
|
|
{showUser && (
|
|
<Pressable style={styles.user} onPress={() => router.push(`/u/${np.user.username}`)} accessibilityRole="link">
|
|
<Avatar user={np.user} size={26} live={np.isPlaying} />
|
|
<Text style={styles.name} numberOfLines={1}>
|
|
{np.user.displayName ?? np.user.username}
|
|
</Text>
|
|
</Pressable>
|
|
)}
|
|
<View style={styles.statusRow}>
|
|
{np.isPlaying && <Equalizer playing color={hero ? '#ffffff' : colors.live} />}
|
|
<Text style={[styles.status, hero && styles.heroText, np.isPlaying && (hero ? styles.heroLive : styles.live)]} numberOfLines={1}>
|
|
{np.isPlaying ? 'Listening now' : `Played ${timeAgo(np.updatedAt)}`}
|
|
<Text
|
|
style={[styles.via, hero && styles.heroText, np.sourceUrl && styles.viaLink]}
|
|
onPress={np.sourceUrl ? () => Linking.openURL(np.sourceUrl!) : undefined}>
|
|
{' · '}
|
|
{np.sourceApp ?? SERVICE_LABELS[np.source]}
|
|
</Text>
|
|
</Text>
|
|
{onTogglePin && (
|
|
<Pressable
|
|
onPress={togglePin}
|
|
hitSlop={12}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={pinned ? 'Unpin from top of feed' : 'Pin to top of feed'}
|
|
accessibilityState={{ selected: pinned }}>
|
|
<Animated.Text style={[styles.pin, pinned && styles.pinOn, starStyle]}>{pinned ? '★' : '☆'}</Animated.Text>
|
|
</Pressable>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
<Animated.View key={np.track.id} entering={FadeIn.duration(250)} exiting={FadeOut.duration(200)}>
|
|
<TrackRow track={np.track} preferred={preferred} expanded={expanded} onToggle={toggle} />
|
|
</Animated.View>
|
|
<ProgressBar np={np} compact={!expanded} />
|
|
<Reactions listenId={np.listenId} reactions={np.reactions} ownerId={np.user.id} commentCount={np.commentCount ?? 0} />
|
|
{expanded && (
|
|
<Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(120)} style={styles.details}>
|
|
<ServiceLinks track={np.track} preferred={preferred} />
|
|
<Lyrics track={np.track} np={np} />
|
|
</Animated.View>
|
|
)}
|
|
</View>
|
|
</OnHero.Provider>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
card: {
|
|
borderRadius: radius.lg,
|
|
padding: space.md + 4,
|
|
gap: space.sm + 2,
|
|
backgroundColor: colors.surface,
|
|
borderWidth: 1,
|
|
borderColor: 'transparent',
|
|
...panelShadow,
|
|
},
|
|
heroCard: { overflow: 'hidden', shadowColor: colors.accent, shadowOpacity: 0.4, shadowRadius: 22 },
|
|
heroText: { color: 'rgba(255,255,255,0.85)' },
|
|
heroLive: { color: '#ffffff', fontFamily: fonts.bodyMedium },
|
|
details: { gap: space.md },
|
|
pinned: { borderColor: colors.accentLine },
|
|
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: space.sm },
|
|
user: { flexDirection: 'row', alignItems: 'center', gap: space.sm, flexShrink: 1 },
|
|
name: { color: colors.text, fontFamily: fonts.bodyMedium, fontSize: 14.5, flexShrink: 1 },
|
|
statusRow: { flexDirection: 'row', alignItems: 'center', gap: space.xs + 2, flexShrink: 1 },
|
|
status: { color: colors.muted, fontSize: 12.5, fontFamily: fonts.body, flexShrink: 1 },
|
|
live: { color: colors.live, fontFamily: fonts.bodyMedium },
|
|
via: { color: colors.muted, fontFamily: fonts.body },
|
|
viaLink: { textDecorationLine: 'underline' },
|
|
pin: { color: colors.muted, fontSize: 21, marginLeft: space.xs },
|
|
pinOn: { color: colors.accent, textShadowColor: 'rgba(255,79,139,0.6)', textShadowRadius: 10 },
|
|
});
|