- Feed and profile cards show a progress bar with elapsed/total time, only when the source reports a real position (Plex, Jellyfin, the Android reader). It ticks locally between updates. Apple Music's guessed position is no longer reported. - The Android reader advances MediaSession's stale position to now, stamps when it was read (the server corrects for upload delay), and reports seeks. The server pushes an update to watchers when a position jumps. - When a polled source says where in the song someone is, the next poll is scheduled just after the song ends (never sooner than 5s), instead of waiting out the full interval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
@@ -17,6 +17,7 @@ import { successHaptic, tapHaptic } from '@/lib/haptics';
|
||||
import { colors, radius, space } from '@/lib/theme';
|
||||
import { Avatar } from './Avatar';
|
||||
import { Equalizer } from './Equalizer';
|
||||
import { ProgressBar } from './ProgressBar';
|
||||
import { TrackCard } from './TrackCard';
|
||||
|
||||
export function NowPlayingCard({
|
||||
@@ -93,6 +94,7 @@ export function NowPlayingCard({
|
||||
<Animated.View key={np.track.id} entering={FadeInDown.duration(380).springify()} exiting={FadeOut.duration(200)}>
|
||||
<TrackCard track={np.track} preferred={preferred} />
|
||||
</Animated.View>
|
||||
<ProgressBar np={np} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
||||
import { formatDuration, playbackPosition, type NowPlayingDTO } from '@musicshare/shared';
|
||||
import { colors } from '@/lib/theme';
|
||||
|
||||
/** Where they are in the song. Renders nothing when the source doesn't report a position. */
|
||||
export function ProgressBar({ np }: { np: NowPlayingDTO }) {
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
const pos = playbackPosition(np, now);
|
||||
const visible = np.isPlaying && pos !== null;
|
||||
const fraction = pos ? pos.positionMs / pos.durationMs : 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [visible]);
|
||||
|
||||
const width = useSharedValue(fraction);
|
||||
useEffect(() => {
|
||||
// Glide between ticks; jump straight there after a seek or a new song.
|
||||
const jump = Math.abs(fraction - width.value) > 0.05;
|
||||
width.set(jump ? fraction : withTiming(fraction, { duration: 1000, easing: Easing.linear }));
|
||||
}, [fraction, width]);
|
||||
const fill = useAnimatedStyle(() => ({ width: `${width.value * 100}%` }));
|
||||
|
||||
if (!visible || !pos) return null;
|
||||
return (
|
||||
<View style={styles.wrap} accessibilityLabel={`${formatDuration(pos.positionMs)} of ${formatDuration(pos.durationMs)}`}>
|
||||
<View style={styles.track}>
|
||||
<Animated.View style={[styles.fill, fill]}>
|
||||
<LinearGradient colors={['#7b2ff7', '#2f8cff']} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }} style={StyleSheet.absoluteFill} />
|
||||
</Animated.View>
|
||||
</View>
|
||||
<View style={styles.times}>
|
||||
<Text style={styles.time}>{formatDuration(pos.positionMs)}</Text>
|
||||
<Text style={styles.time}>{formatDuration(pos.durationMs)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrap: { marginTop: 12 },
|
||||
track: { height: 4, borderRadius: 2, backgroundColor: 'rgba(255,255,255,0.14)', overflow: 'hidden' },
|
||||
fill: { height: '100%', borderRadius: 2, overflow: 'hidden' },
|
||||
times: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 4 },
|
||||
time: { color: colors.muted, fontSize: 12, fontVariant: ['tabular-nums'] },
|
||||
});
|
||||
Reference in New Issue
Block a user