mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-12 05:10:52 +02:00
160 lines
4.4 KiB
TypeScript
160 lines
4.4 KiB
TypeScript
import { useState } from 'react';
|
|
import { View, Pressable, StyleSheet, type LayoutChangeEvent } from 'react-native';
|
|
import { Image } from 'expo-image';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { useRouter } from 'expo-router';
|
|
import { Text } from './Text';
|
|
import { AstraLogo } from './AstraLogo';
|
|
import { SpectrumCurve } from './SpectrumCurve';
|
|
import { colors, radius, spacing } from '@/theme';
|
|
import { usePlayerStore } from '@/stores/playerStore';
|
|
import { skipToNext, togglePlay } from '@/audio/playbackController';
|
|
import { useScopeActive } from '@/scope/scopeStore';
|
|
import { useSpectrumCurve } from '@/scope/useSpectrumCurve';
|
|
|
|
const PILL_HEIGHT = 56;
|
|
const ART = 42;
|
|
const CURVE_POINTS = 64;
|
|
|
|
/**
|
|
* Persistent floating mini-player (M3 redesign): a rounded pill above the tab
|
|
* bar with the live filled-line spectrum drifting behind the metadata. Tapping
|
|
* opens the full now-playing screen.
|
|
*/
|
|
export function MiniPlayer() {
|
|
const router = useRouter();
|
|
const track = usePlayerStore((s) => s.currentTrack);
|
|
const playbackState = usePlayerStore((s) => s.playbackState);
|
|
const currentTime = usePlayerStore((s) => s.currentTime);
|
|
const duration = usePlayerStore((s) => s.duration);
|
|
|
|
const scopeActive = useScopeActive();
|
|
const values = useSpectrumCurve(CURVE_POINTS, scopeActive);
|
|
const [pillWidth, setPillWidth] = useState(0);
|
|
|
|
if (!track) return null;
|
|
|
|
const isPlaying = playbackState === 'playing';
|
|
const isLoading = playbackState === 'loading';
|
|
const progress = duration > 0 ? Math.min(1, currentTime / duration) : 0;
|
|
|
|
const onLayout = (e: LayoutChangeEvent) => setPillWidth(e.nativeEvent.layout.width);
|
|
|
|
return (
|
|
<Pressable style={styles.pill} onPress={() => router.push('/now-playing')} onLayout={onLayout}>
|
|
{scopeActive && pillWidth > 0 && (
|
|
<View pointerEvents="none" style={styles.spectrum}>
|
|
<SpectrumCurve
|
|
values={values}
|
|
width={pillWidth}
|
|
height={PILL_HEIGHT}
|
|
lineWidth={1.5}
|
|
fillOpacity={0.5}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
<View style={styles.row}>
|
|
<View style={styles.art}>
|
|
{track.artworkData ? (
|
|
<Image source={{ uri: track.artworkData }} style={styles.artImage} contentFit="cover" />
|
|
) : (
|
|
<AstraLogo size={20} />
|
|
)}
|
|
</View>
|
|
|
|
<View style={styles.meta}>
|
|
<Text variant="body" numberOfLines={1} style={styles.title}>
|
|
{track.title}
|
|
</Text>
|
|
<Text variant="label" numberOfLines={1}>
|
|
{track.artist}
|
|
</Text>
|
|
</View>
|
|
|
|
<Pressable hitSlop={10} onPress={togglePlay} style={styles.control}>
|
|
<Ionicons
|
|
name={isLoading ? 'ellipsis-horizontal' : isPlaying ? 'pause' : 'play'}
|
|
size={24}
|
|
color={colors.accent}
|
|
/>
|
|
</Pressable>
|
|
<Pressable hitSlop={10} onPress={skipToNext} style={styles.control}>
|
|
<Ionicons name="play-skip-forward" size={22} color={colors.textPrimary} />
|
|
</Pressable>
|
|
</View>
|
|
|
|
<View style={styles.progressTrack}>
|
|
<View style={[styles.progressFill, { width: `${progress * 100}%` }]} />
|
|
</View>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
pill: {
|
|
height: PILL_HEIGHT,
|
|
marginHorizontal: spacing.md,
|
|
marginTop: spacing.sm,
|
|
marginBottom: spacing.sm,
|
|
borderRadius: radius.lg,
|
|
backgroundColor: colors.bgTertiary,
|
|
borderColor: colors.glassBorder,
|
|
borderWidth: StyleSheet.hairlineWidth,
|
|
overflow: 'hidden',
|
|
justifyContent: 'center',
|
|
},
|
|
spectrum: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
},
|
|
row: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: spacing.sm,
|
|
gap: spacing.sm,
|
|
},
|
|
art: {
|
|
width: ART,
|
|
height: ART,
|
|
borderRadius: radius.sm,
|
|
backgroundColor: colors.bgSecondary,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
overflow: 'hidden',
|
|
},
|
|
artImage: {
|
|
width: '100%',
|
|
height: '100%',
|
|
},
|
|
meta: {
|
|
flex: 1,
|
|
},
|
|
title: {
|
|
fontSize: 15,
|
|
},
|
|
control: {
|
|
width: 36,
|
|
height: 36,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
progressTrack: {
|
|
position: 'absolute',
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
height: 2,
|
|
backgroundColor: colors.glassBorder,
|
|
},
|
|
progressFill: {
|
|
height: 2,
|
|
backgroundColor: colors.accent,
|
|
},
|
|
});
|
|
|
|
export default MiniPlayer;
|