mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +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:
@@ -1,5 +1,12 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { formatTrackTime, formatLongDuration } from './formatDuration';
|
||||
import {
|
||||
formatTrackTime,
|
||||
formatLongDuration,
|
||||
formatPlaybarClock,
|
||||
formatPlaybarToggleClock,
|
||||
playbarMinuteFieldWidth,
|
||||
PLAYBAR_CLOCK_PAD,
|
||||
} from './formatDuration';
|
||||
|
||||
describe('formatTrackTime', () => {
|
||||
it('formats m:ss with zero-padded seconds', () => {
|
||||
@@ -51,3 +58,44 @@ describe('formatLongDuration', () => {
|
||||
expect(formatLongDuration(-1)).toBe('0:00');
|
||||
});
|
||||
});
|
||||
|
||||
describe('playbarMinuteFieldWidth', () => {
|
||||
it('matches the minute digit count of the track duration', () => {
|
||||
expect(playbarMinuteFieldWidth(65)).toBe(1);
|
||||
expect(playbarMinuteFieldWidth(599)).toBe(1);
|
||||
expect(playbarMinuteFieldWidth(600)).toBe(2);
|
||||
expect(playbarMinuteFieldWidth(3600)).toBe(2);
|
||||
expect(playbarMinuteFieldWidth(6000)).toBe(3);
|
||||
});
|
||||
|
||||
it('returns 1 for invalid duration', () => {
|
||||
expect(playbarMinuteFieldWidth(0)).toBe(1);
|
||||
expect(playbarMinuteFieldWidth(NaN)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPlaybarClock', () => {
|
||||
it('pads minutes with figure spaces to a fixed field width', () => {
|
||||
expect(formatPlaybarClock(65, 1)).toBe('1:05');
|
||||
expect(formatPlaybarClock(599, 2)).toBe(`${PLAYBAR_CLOCK_PAD}9:59`);
|
||||
expect(formatPlaybarClock(600, 2)).toBe('10:00');
|
||||
});
|
||||
|
||||
it('keeps remaining/elapsed strings the same length while ticking', () => {
|
||||
expect(formatPlaybarClock(599, 2).length).toBe(formatPlaybarClock(600, 2).length);
|
||||
expect(formatPlaybarClock(59, 2).length).toBe(formatPlaybarClock(60, 2).length);
|
||||
});
|
||||
|
||||
it('returns a padded zero clock for invalid input', () => {
|
||||
expect(formatPlaybarClock(0, 2)).toBe(`${PLAYBAR_CLOCK_PAD}0:00`);
|
||||
expect(formatPlaybarClock(NaN, 3)).toBe(`${PLAYBAR_CLOCK_PAD.repeat(2)}0:00`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPlaybarToggleClock', () => {
|
||||
it('prefixes remaining with minus and duration with a figure space', () => {
|
||||
expect(formatPlaybarToggleClock(65, 1, true)).toBe('-1:05');
|
||||
expect(formatPlaybarToggleClock(65, 1, false)).toBe(`${PLAYBAR_CLOCK_PAD}1:05`);
|
||||
expect(formatPlaybarToggleClock(65, 1, true).length).toBe(formatPlaybarToggleClock(65, 1, false).length);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,3 +21,43 @@ export function formatLongDuration(seconds: number): string {
|
||||
if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/** Invisible same-width pad for fixed-length playbar clocks (no leading zeros). */
|
||||
export const PLAYBAR_CLOCK_PAD = '\u2007';
|
||||
|
||||
/** Minute digit count for a fixed-width playbar clock derived from track duration. */
|
||||
export function playbarMinuteFieldWidth(durationSeconds: number): number {
|
||||
if (!durationSeconds || !isFinite(durationSeconds) || durationSeconds <= 0) return 1;
|
||||
return Math.max(1, String(Math.floor(durationSeconds / 60)).length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed-width `m:ss` for the player seekbar — minutes padded with figure spaces
|
||||
* so digit columns do not shift while the clock ticks (WebKit waveform resize).
|
||||
*/
|
||||
export function formatPlaybarClock(
|
||||
seconds: number,
|
||||
minuteFieldWidth: number,
|
||||
fallback = '0:00',
|
||||
): string {
|
||||
if (!seconds || !isFinite(seconds) || seconds < 0) {
|
||||
if (fallback === '0:00') {
|
||||
return `${PLAYBAR_CLOCK_PAD.repeat(Math.max(0, minuteFieldWidth - 1))}0:00`;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
const clamped = Math.max(0, seconds);
|
||||
const m = Math.floor(clamped / 60);
|
||||
const s = Math.floor(clamped % 60);
|
||||
return `${String(m).padStart(minuteFieldWidth, PLAYBAR_CLOCK_PAD)}:${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/** Right-hand playbar toggle: `-m:ss` (remaining) or figure-space + `m:ss` (duration). */
|
||||
export function formatPlaybarToggleClock(
|
||||
seconds: number,
|
||||
minuteFieldWidth: number,
|
||||
remaining: boolean,
|
||||
): string {
|
||||
const body = formatPlaybarClock(seconds, minuteFieldWidth);
|
||||
return remaining ? `-${body}` : `${PLAYBAR_CLOCK_PAD}${body}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user