mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
fix(player): stable playbar clocks when showing remaining time (#987)
* chore: restore PR order in CHANGELOG [1.47.0] Fixed section Re-sort ### blocks ascending by PR number per release-note placement rules after out-of-order Discord-fix batch inserts. * fix(player): stable playbar clocks when showing remaining time Pad seekbar time strings to a fixed width so ticking remaining time does not resize WaveformSeek via ResizeObserver; tighten clock-to-waveform spacing and keep the toggle icon inline. * docs: CHANGELOG and credits for playbar remaining-time fix (PR #987) * docs: CHANGELOG entry for playbar remaining-time waveform fix (#987) * chore: drop settingsCredits entry for minor playbar fix (#987) * docs: move #987 CHANGELOG entry to end of [1.47.0] Fixed section
This commit is contained in:
@@ -30,7 +30,6 @@ import { usePlaybackScheduleRemaining } from '../utils/format/playbackScheduleFo
|
||||
import { usePreviewStore } from '../store/previewStore';
|
||||
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
|
||||
import { formatTrackTime } from '../utils/format/formatDuration';
|
||||
import { PlaybackTime, RemainingTime } from './playerBar/PlaybackClock';
|
||||
import { PlayerTrackInfo } from './playerBar/PlayerTrackInfo';
|
||||
import { PlayerTransportControls } from './playerBar/PlayerTransportControls';
|
||||
import { PlayerSeekbarSection } from './playerBar/PlayerSeekbarSection';
|
||||
|
||||
@@ -1,35 +1,59 @@
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../../store/playbackProgress';
|
||||
import { formatTrackTime } from '../../utils/format/formatDuration';
|
||||
import {
|
||||
formatPlaybarClock,
|
||||
formatPlaybarToggleClock,
|
||||
formatTrackTime,
|
||||
} from '../../utils/format/formatDuration';
|
||||
|
||||
/** Renders the playback clock without ever causing PlayerBar to re-render.
|
||||
* Updates the DOM directly via an imperative store subscription. */
|
||||
export const PlaybackTime = memo(function PlaybackTime({ className }: { className?: string }) {
|
||||
export const PlaybackTime = memo(function PlaybackTime({
|
||||
className,
|
||||
minuteFieldWidth,
|
||||
}: {
|
||||
className?: string;
|
||||
minuteFieldWidth?: number;
|
||||
}) {
|
||||
const spanRef = useRef<HTMLSpanElement>(null);
|
||||
useEffect(() => {
|
||||
const format = (seconds: number) =>
|
||||
minuteFieldWidth != null
|
||||
? formatPlaybarClock(seconds, minuteFieldWidth)
|
||||
: formatTrackTime(seconds);
|
||||
if (spanRef.current) {
|
||||
spanRef.current.textContent = formatTrackTime(getPlaybackProgressSnapshot().currentTime);
|
||||
spanRef.current.textContent = format(getPlaybackProgressSnapshot().currentTime);
|
||||
}
|
||||
return subscribePlaybackProgress(state => {
|
||||
if (spanRef.current) spanRef.current.textContent = formatTrackTime(state.currentTime);
|
||||
if (spanRef.current) spanRef.current.textContent = format(state.currentTime);
|
||||
});
|
||||
}, []);
|
||||
}, [minuteFieldWidth]);
|
||||
return <span className={className} ref={spanRef} />;
|
||||
});
|
||||
|
||||
/** Renders the remaining time (duration - currentTime) without causing PlayerBar
|
||||
* to re-render. */
|
||||
export const RemainingTime = memo(function RemainingTime({ duration, className }: { duration: number; className?: string }) {
|
||||
/** Remaining/duration toggle clock — fixed-length string, imperative updates. */
|
||||
export const ToggleClock = memo(function ToggleClock({
|
||||
duration,
|
||||
minuteFieldWidth,
|
||||
remaining,
|
||||
className,
|
||||
}: {
|
||||
duration: number;
|
||||
minuteFieldWidth: number;
|
||||
remaining: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const spanRef = useRef<HTMLSpanElement>(null);
|
||||
useEffect(() => {
|
||||
const updateRemaining = () => {
|
||||
if (spanRef.current) {
|
||||
const remaining = Math.max(0, duration - getPlaybackProgressSnapshot().currentTime);
|
||||
spanRef.current.textContent = `-${formatTrackTime(remaining)}`;
|
||||
}
|
||||
const update = () => {
|
||||
if (!spanRef.current) return;
|
||||
const seconds = remaining
|
||||
? Math.max(0, duration - getPlaybackProgressSnapshot().currentTime)
|
||||
: duration;
|
||||
spanRef.current.textContent = formatPlaybarToggleClock(seconds, minuteFieldWidth, remaining);
|
||||
};
|
||||
updateRemaining();
|
||||
return subscribePlaybackProgress(updateRemaining);
|
||||
}, [duration]);
|
||||
update();
|
||||
return remaining ? subscribePlaybackProgress(update) : undefined;
|
||||
}, [duration, minuteFieldWidth, remaining]);
|
||||
return <span className={className} ref={spanRef} />;
|
||||
});
|
||||
|
||||
@@ -2,9 +2,9 @@ import { ArrowLeftRight } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { RadioMetadata } from '../../hooks/useRadioMetadata';
|
||||
import { useThemeStore } from '../../store/themeStore';
|
||||
import { formatTrackTime } from '../../utils/format/formatDuration';
|
||||
import { formatTrackTime, playbarMinuteFieldWidth } from '../../utils/format/formatDuration';
|
||||
import WaveformSeek from '../WaveformSeek';
|
||||
import { PlaybackTime, RemainingTime } from './PlaybackClock';
|
||||
import { PlaybackTime, ToggleClock } from './PlaybackClock';
|
||||
|
||||
interface Props {
|
||||
isRadio: boolean;
|
||||
@@ -21,8 +21,14 @@ export function PlayerSeekbarSection({
|
||||
isRadio, radioMeta, trackId, duration, localShowRemaining, setLocalShowRemaining,
|
||||
disableWaveformCanvas, t,
|
||||
}: Props) {
|
||||
const minuteFieldWidth = playbarMinuteFieldWidth(duration);
|
||||
const playbarClockStyle = {
|
||||
'--playbar-clock-body-ch': minuteFieldWidth + 3,
|
||||
'--playbar-clock-signed-ch': minuteFieldWidth + 4,
|
||||
} as React.CSSProperties;
|
||||
|
||||
return (
|
||||
<div className="player-waveform-section">
|
||||
<div className="player-waveform-section" style={playbarClockStyle}>
|
||||
{isRadio ? (
|
||||
<>
|
||||
{radioMeta.source === 'azuracast' && radioMeta.elapsed != null && radioMeta.duration != null && radioMeta.duration > 0 ? (
|
||||
@@ -50,7 +56,7 @@ export function PlayerSeekbarSection({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlaybackTime className="player-time" />
|
||||
<PlaybackTime className="player-time" minuteFieldWidth={minuteFieldWidth} />
|
||||
<div className="player-waveform-wrap">
|
||||
{disableWaveformCanvas
|
||||
? <div className="radio-progress-bar" aria-hidden />
|
||||
@@ -65,8 +71,13 @@ export function PlayerSeekbarSection({
|
||||
}}
|
||||
data-tooltip={localShowRemaining ? t('player.showDuration') : t('player.showRemainingTime')}
|
||||
>
|
||||
{localShowRemaining ? <RemainingTime duration={duration} /> : formatTrackTime(duration)}
|
||||
<ArrowLeftRight size={10} style={{ marginLeft: 4, opacity: 0.6 }} />
|
||||
<ToggleClock
|
||||
className="player-time-toggle__label"
|
||||
duration={duration}
|
||||
minuteFieldWidth={minuteFieldWidth}
|
||||
remaining={localShowRemaining}
|
||||
/>
|
||||
<ArrowLeftRight className="player-time-toggle__icon" size={10} aria-hidden />
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user