Keep progress bars aligned
CI / check (push) Canceled after 0s

The server now sends each fresh position on every poll that has one, and
sends positions advanced to send time; clients count only the time since
they received it. Bars realign every poll and no longer drift by however
far the viewer's clock is off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
2026-10-06 12:48:29 -04:00
co-authored by Claude Opus 5.5
parent be0a00a2c1
commit 13df568348
7 changed files with 93 additions and 67 deletions
+12 -10
View File
@@ -7,26 +7,28 @@ 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;
if (!np.isPlaying || playbackPosition(np, 0) === null) return null;
// Each fresh position from the server restarts the clock, realigning the bar.
return <Bar key={`${np.track.id}|${np.progressMs}|${np.updatedAt}`} np={np} />;
}
function Bar({ np }: { np: NowPlayingDTO }) {
const [receivedAt] = useState(() => Date.now());
const [now, setNow] = useState(receivedAt);
useEffect(() => {
if (!visible) return;
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, [visible]);
}, []);
const pos = playbackPosition(np, now - receivedAt);
const fraction = pos ? pos.positionMs / pos.durationMs : 0;
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 }));
width.set(withTiming(fraction, { duration: 1000, easing: Easing.linear })); // glide between ticks
}, [fraction, width]);
const fill = useAnimatedStyle(() => ({ width: `${width.value * 100}%` }));
if (!visible || !pos) return null;
if (!pos) return null;
return (
<View style={styles.wrap} accessibilityLabel={`${formatDuration(pos.positionMs)} of ${formatDuration(pos.durationMs)}`}>
<View style={styles.track}>