refactor(format): consolidate duration formatters into format/formatDuration (Phase L, part 2) (#690)

The mm:ss track-time formatter was hand-rolled in 11 places and the
h:mm:ss total-duration formatter in 4 — extract two tested functions:

- formatTrackTime(seconds, fallback='0:00')  — m:ss, used for track /
  playback times. fallback param covers the '–' placeholder rows.
- formatLongDuration(seconds)                — h:mm:ss when >=1h, else
  m:ss, used for album / queue totals.

Behaviour preserved per call site: the unified guard
(!seconds || !isFinite || <0 -> fallback) produces identical output to
every prior variant for all real inputs; SongRow keeps its '–' via the
fallback arg. Removes the formatter exports from 6 componentHelpers
files (playerBarHelpers / fullscreenPlayerHelpers deleted — they only
exported the formatter) and 7 inline component copies.

+ formatDuration.test.ts
This commit is contained in:
Frank Stellmacher
2026-05-14 14:50:10 +02:00
committed by GitHub
parent 7a7a9f5e6b
commit 5231169a71
34 changed files with 139 additions and 138 deletions
+2 -9
View File
@@ -12,14 +12,7 @@ import StarRating from './StarRating';
import { copyEntityShareLink } from '../utils/share/copyEntityShareLink';
import { showToast } from '../utils/ui/toast';
import { isAlbumRecentlyAdded } from '../utils/albumRecency';
function formatDuration(seconds: number): string {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
return `${m}:${s.toString().padStart(2, '0')}`;
}
import { formatLongDuration } from '../utils/format/formatDuration';
function formatSize(bytes?: number): string {
if (!bytes) return '';
@@ -208,7 +201,7 @@ export default function AlbumHeader({
{info.year && <span>{info.year}</span>}
{info.genre && <span>· {info.genre}</span>}
<span>· {songs.length} Tracks</span>
<span>· {formatDuration(totalDuration)}</span>
<span>· {formatLongDuration(totalDuration)}</span>
{formatLabel && <span>· {formatLabel}</span>}
{info.recordLabel && (
<>
+4 -10
View File
@@ -12,6 +12,7 @@ import {
} from 'lucide-react';
import { usePlayerStore } from '../store/playerStore';
import { useCachedUrl } from './CachedImage';
import { formatTrackTime } from '../utils/format/formatDuration';
import LyricsPane from './LyricsPane';
import { usePlaybackDelayPress } from '../hooks/usePlaybackDelayPress';
import PlaybackDelayModal from './PlaybackDelayModal';
@@ -66,13 +67,6 @@ function useAlbumAccentColor(imageUrl: string): string {
return color;
}
function formatTime(seconds: number): string {
if (!seconds || isNaN(seconds)) return '0:00';
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
}
// ── Queue Drawer ──────────────────────────────────────────────────────────────
function QueueDrawer({ onClose }: { onClose: () => void }) {
@@ -119,7 +113,7 @@ function QueueDrawer({ onClose }: { onClose: () => void }) {
</div>
<div className="mq-item-artist truncate">{track.artist}</div>
</div>
<span className="mq-item-dur">{formatTime(track.duration)}</span>
<span className="mq-item-dur">{formatTrackTime(track.duration)}</span>
</div>
);
})
@@ -373,8 +367,8 @@ export default function MobilePlayerView() {
<div className="mp-scrubber-thumb" style={{ left: `${effectiveProgress * 100}%` }} />
</div>
<div className="mp-scrubber-times">
<span>{formatTime(effectiveTime)}</span>
<span>-{formatTime(Math.max(0, duration - effectiveTime))}</span>
<span>{formatTrackTime(effectiveTime)}</span>
<span>-{formatTrackTime(Math.max(0, duration - effectiveTime))}</span>
</div>
</div>
+3 -4
View File
@@ -19,6 +19,7 @@ import OrbitSettingsPopover from './OrbitSettingsPopover';
import OrbitSharePopover from './OrbitSharePopover';
import OrbitDiagnosticsPopover from './OrbitDiagnosticsPopover';
import ConfirmModal from './ConfirmModal';
import { formatTrackTime } from '../utils/format/formatDuration';
/**
* Orbit — top-strip session indicator.
@@ -34,11 +35,9 @@ import ConfirmModal from './ConfirmModal';
const CATCH_UP_DRIFT_THRESHOLD_MS = 3_000;
/** `m:ss` countdown from a millisecond value. */
function formatCountdown(ms: number): string {
const clamped = Math.max(0, Math.round(ms / 1000));
const m = Math.floor(clamped / 60);
const s = clamped % 60;
return `${m}:${s.toString().padStart(2, '0')}`;
return formatTrackTime(Math.round(ms / 1000));
}
export default function OrbitSessionBar() {
+1 -1
View File
@@ -28,7 +28,7 @@ import PlaybackScheduleBadge from './PlaybackScheduleBadge';
import { usePlaybackScheduleRemaining } from '../utils/format/playbackScheduleFormat';
import { usePreviewStore } from '../store/previewStore';
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
import { formatTime } from '../utils/componentHelpers/playerBarHelpers';
import { formatTrackTime } from '../utils/format/formatDuration';
import { PlaybackTime, RemainingTime } from './playerBar/PlaybackClock';
import { PlayerTrackInfo } from './playerBar/PlayerTrackInfo';
import { PlayerTransportControls } from './playerBar/PlayerTransportControls';
+1 -4
View File
@@ -23,10 +23,7 @@ import NowPlayingInfo from './NowPlayingInfo';
import { TFunction } from 'i18next';
import { useLuckyMixStore } from '../store/luckyMixStore';
import { useQueueToolbarStore } from '../store/queueToolbarStore';
import {
DurationMode,
formatTime,
} from '../utils/componentHelpers/queuePanelHelpers';
import { DurationMode } from '../utils/componentHelpers/queuePanelHelpers';
import { SavePlaylistModal } from './queuePanel/SavePlaylistModal';
import { LoadPlaylistModal } from './queuePanel/LoadPlaylistModal';
import { QueueHeader } from './queuePanel/QueueHeader';
+2 -7
View File
@@ -10,12 +10,7 @@ import { useAuthStore } from '../store/authStore';
import { useTranslation } from 'react-i18next';
import { copyTextToClipboard } from '../utils/server/serverMagicString';
import { showToast } from '../utils/ui/toast';
function formatDuration(s: number): string {
const m = Math.floor(s / 60);
const sec = s % 60;
return `${m}:${sec.toString().padStart(2, '0')}`;
}
import { formatTrackTime } from '../utils/format/formatDuration';
function formatSize(bytes?: number): string | null {
if (!bytes) return null;
@@ -152,7 +147,7 @@ export default function SongInfoModal() {
)}
<Row label={t('songInfo.year')} value={song.year} />
<Row label={t('songInfo.genre')} value={song.genre} />
<Row label={t('songInfo.duration')} value={formatDuration(song.duration)} />
<Row label={t('songInfo.duration')} value={formatTrackTime(song.duration)} />
<Row label={t('songInfo.track')} value={trackLabel} />
<Divider />
+2 -8
View File
@@ -8,13 +8,7 @@ import { usePlayerStore } from '../store/playerStore';
import { enqueueAndPlay } from '../utils/playback/playSong';
import { useDragDrop } from '../contexts/DragDropContext';
import { useOrbitSongRowBehavior } from '../hooks/useOrbitSongRowBehavior';
function fmtDuration(s: number): string {
if (!s || !isFinite(s)) return '';
const m = Math.floor(s / 60);
const sec = Math.floor(s % 60);
return `${m}:${sec.toString().padStart(2, '0')}`;
}
import { formatTrackTime } from '../utils/format/formatDuration';
interface Props {
song: SubsonicSong;
@@ -108,7 +102,7 @@ function SongRow({ song }: Props) {
<div className="song-list-row-cell song-list-row-genre truncate" title={song.genre ?? ''}>
{song.genre ?? '—'}
</div>
<div className="song-list-row-cell song-list-row-duration">{fmtDuration(song.duration)}</div>
<div className="song-list-row-cell song-list-row-duration">{formatTrackTime(song.duration, '')}</div>
</div>
);
}
@@ -3,7 +3,7 @@ import { AudioLines } from 'lucide-react';
import type { SubsonicSong } from '../../api/subsonicTypes';
import type { Track } from '../../store/playerStoreTypes';
import { songToTrack } from '../../utils/playback/songToTrack';
import { formatDuration } from '../../utils/componentHelpers/albumTrackListHelpers';
import { formatLongDuration } from '../../utils/format/formatDuration';
interface Props {
discNums: number[];
@@ -71,7 +71,7 @@ export function AlbumTrackListMobile({
)}
<span className="tracklist-mobile-title">{song.title}</span>
</div>
<span className="tracklist-mobile-duration">{formatDuration(song.duration)}</span>
<span className="tracklist-mobile-duration">{formatLongDuration(song.duration)}</span>
</div>
);
})}
+3 -2
View File
@@ -10,7 +10,8 @@ import { useSelectionStore } from '../../store/selectionStore';
import { useThemeStore } from '../../store/themeStore';
import { usePreviewStore } from '../../store/previewStore';
import StarRating from '../StarRating';
import { codecLabel, formatDuration, type ColKey } from '../../utils/componentHelpers/albumTrackListHelpers';
import { codecLabel, type ColKey } from '../../utils/componentHelpers/albumTrackListHelpers';
import { formatLongDuration } from '../../utils/format/formatDuration';
type ContextMenuFn = (
x: number,
@@ -173,7 +174,7 @@ export const TrackRow = React.memo(function TrackRow({
case 'duration':
return (
<div key="duration" className="track-duration">
{formatDuration(song.duration)}
{formatLongDuration(song.duration)}
</div>
);
case 'format':
@@ -6,7 +6,7 @@ import { usePlayerStore } from '../../store/playerStore';
import { usePreviewStore } from '../../store/previewStore';
import { useOrbitSongRowBehavior } from '../../hooks/useOrbitSongRowBehavior';
import { songToTrack } from '../../utils/playback/songToTrack';
import { formatDuration } from '../../utils/componentHelpers/artistDetailHelpers';
import { formatTrackTime } from '../../utils/format/formatDuration';
import ArtistSuggestionTrackCover from './ArtistSuggestionTrackCover';
interface Props {
@@ -100,7 +100,7 @@ export default function ArtistDetailTopTracks({ topSongs, marginTop, playTopSong
{song.album}
</div>
<div className="track-duration" style={{ textAlign: 'right' }}>
{formatDuration(song.duration)}
{formatTrackTime(song.duration)}
</div>
</div>
);
@@ -13,6 +13,7 @@ import { useSelectionStore } from '../../store/selectionStore';
import { useDragDrop } from '../../contexts/DragDropContext';
import { useOrbitSongRowBehavior } from '../../hooks/useOrbitSongRowBehavior';
import { songToTrack } from '../../utils/playback/songToTrack';
import { formatTrackTime } from '../../utils/format/formatDuration';
import { AddToPlaylistSubmenu } from '../ContextMenu';
import StarRating from '../StarRating';
@@ -348,7 +349,7 @@ export default function FavoritesSongsTracklist({
);
case 'duration': return (
<div key="duration" className="track-duration">
{Math.floor(song.duration / 60)}:{(song.duration % 60).toString().padStart(2, '0')}
{formatTrackTime(song.duration)}
</div>
);
case 'remove': return (
@@ -1,7 +1,7 @@
import React, { memo, useCallback, useEffect, useRef } from 'react';
import { usePlayerStore } from '../../store/playerStore';
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../../store/playbackProgress';
import { formatTime } from '../../utils/componentHelpers/fullscreenPlayerHelpers';
import { formatTrackTime } from '../../utils/format/formatDuration';
// Full-width seekbar — imperative DOM updates, zero React re-renders on tick.
export const FsSeekbar = memo(function FsSeekbar({ duration }: { duration: number }) {
@@ -19,7 +19,7 @@ export const FsSeekbar = memo(function FsSeekbar({ duration }: { duration: numbe
pendingSeekRef.current = p;
if (timeRef.current) {
const previewTime = duration > 0 ? p * duration : s.currentTime;
timeRef.current.textContent = formatTime(previewTime);
timeRef.current.textContent = formatTrackTime(previewTime);
}
if (playedRef.current) playedRef.current.style.width = `${p * 100}%`;
if (bufRef.current) bufRef.current.style.width = `${Math.max(p * 100, s.buffered * 100)}%`;
@@ -36,7 +36,7 @@ export const FsSeekbar = memo(function FsSeekbar({ duration }: { duration: numbe
useEffect(() => {
const s = getPlaybackProgressSnapshot();
const pct = s.progress * 100;
if (timeRef.current) timeRef.current.textContent = formatTime(s.currentTime);
if (timeRef.current) timeRef.current.textContent = formatTrackTime(s.currentTime);
if (playedRef.current) playedRef.current.style.width = `${pct}%`;
if (bufRef.current) bufRef.current.style.width = `${Math.max(pct, s.buffered * 100)}%`;
if (inputRef.current) inputRef.current.value = String(s.progress);
@@ -44,7 +44,7 @@ export const FsSeekbar = memo(function FsSeekbar({ duration }: { duration: numbe
return subscribePlaybackProgress(state => {
if (isDraggingRef.current) return;
const p = state.progress * 100;
if (timeRef.current) timeRef.current.textContent = formatTime(state.currentTime);
if (timeRef.current) timeRef.current.textContent = formatTrackTime(state.currentTime);
if (playedRef.current) playedRef.current.style.width = `${p}%`;
if (bufRef.current) bufRef.current.style.width = `${Math.max(p, state.buffered * 100)}%`;
if (inputRef.current) inputRef.current.value = String(state.progress);
@@ -62,7 +62,7 @@ export const FsSeekbar = memo(function FsSeekbar({ duration }: { duration: numbe
<div className="fs-seekbar-wrap">
<div className="fs-seekbar-times">
<span ref={timeRef} />
<span>{formatTime(duration)}</span>
<span>{formatTrackTime(duration)}</span>
</div>
<div className="fs-seekbar">
<div className="fs-seekbar-bg" />
+3 -3
View File
@@ -1,5 +1,5 @@
import { Pause, Play, SkipBack, SkipForward } from 'lucide-react';
import { fmt } from '../../utils/componentHelpers/miniPlayerHelpers';
import { formatTrackTime } from '../../utils/format/formatDuration';
import type { MiniControlAction } from '../../utils/miniPlayerBridge';
interface Props {
@@ -26,11 +26,11 @@ export function MiniControls({ isPlaying, currentTime, duration, progress, contr
</div>
<div className="mini-player__progress">
<div className="mini-player__progress-time">{fmt(currentTime)}</div>
<div className="mini-player__progress-time">{formatTrackTime(currentTime)}</div>
<div className="mini-player__progress-track">
<div className="mini-player__progress-fill" style={{ width: `${progress}%` }} />
</div>
<div className="mini-player__progress-time">{fmt(duration)}</div>
<div className="mini-player__progress-time">{formatTrackTime(duration)}</div>
</div>
</div>
);
+3 -2
View File
@@ -2,7 +2,8 @@ import React, { memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Disc3, ExternalLink, Star } from 'lucide-react';
import type { SubsonicAlbum, SubsonicSong } from '../../api/subsonicTypes';
import { formatTime, formatTotalDuration } from '../../utils/componentHelpers/nowPlayingHelpers';
import { formatTotalDuration } from '../../utils/componentHelpers/nowPlayingHelpers';
import { formatTrackTime } from '../../utils/format/formatDuration';
interface AlbumCardProps {
album: SubsonicAlbum | null;
@@ -79,7 +80,7 @@ const AlbumCard = memo(function AlbumCard({ album, songs, currentTrackId, albumN
: track.track ?? '—'}
</span>
<span className="np-album-track-title truncate">{track.title}</span>
<span className="np-album-track-dur">{formatTime(track.duration)}</span>
<span className="np-album-track-dur">{formatTrackTime(track.duration)}</span>
</div>
);
})}
+2 -2
View File
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Headphones, Heart, MicVocal, Music, Star } from 'lucide-react';
import type { LastfmArtistStats, LastfmTrackInfo } from '../../api/lastfm';
import LastfmIcon from '../LastfmIcon';
import { formatTime } from '../../utils/componentHelpers/nowPlayingHelpers';
import { formatTrackTime } from '../../utils/format/formatDuration';
interface HeroProps {
track: { title: string; artist: string; album: string; year?: number;
@@ -83,7 +83,7 @@ const Hero = memo(function Hero({ track, genre, playCount, userRatingOverride, l
{track.samplingRate && <span className="np-badge">{(track.samplingRate / 1000).toFixed(1)} kHz</span>}
{track.bitDepth && <span className="np-badge">{track.bitDepth}-bit</span>}
{hiRes && <span className="np-badge np-badge-hires">Hi-Res</span>}
{track.duration > 0 && <span className="np-badge">{formatTime(track.duration)}</span>}
{track.duration > 0 && <span className="np-badge">{formatTrackTime(track.duration)}</span>}
</div>
<div className="np-dash-hero-actions">
+3 -3
View File
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Cast, Clock, Radio, SkipForward, Users } from 'lucide-react';
import type { useRadioMetadata } from '../../hooks/useRadioMetadata';
import { usePlayerStore } from '../../store/playerStore';
import { formatTime } from '../../utils/componentHelpers/nowPlayingHelpers';
import { formatTrackTime } from '../../utils/format/formatDuration';
type NonNullStoreField<K extends keyof ReturnType<typeof usePlayerStore.getState>> =
NonNullable<ReturnType<typeof usePlayerStore.getState>[K]>;
@@ -38,11 +38,11 @@ const RadioView = memo(function RadioView({ radioMeta, currentRadio, resolvedCov
</div>
{radioMeta.source === 'azuracast' && radioMeta.elapsed != null && radioMeta.duration != null && radioMeta.duration > 0 && (
<div className="np-radio-progress-wrap">
<span className="np-radio-time">{formatTime(radioMeta.elapsed)}</span>
<span className="np-radio-time">{formatTrackTime(radioMeta.elapsed)}</span>
<div className="np-radio-progress-bar">
<div className="np-radio-progress-fill" style={{ width: `${Math.min(100, (radioMeta.elapsed / radioMeta.duration) * 100)}%` }} />
</div>
<span className="np-radio-time">{formatTime(radioMeta.duration)}</span>
<span className="np-radio-time">{formatTrackTime(radioMeta.duration)}</span>
</div>
)}
</div>
+2 -2
View File
@@ -2,7 +2,7 @@ import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { ExternalLink, Play, TrendingUp } from 'lucide-react';
import type { SubsonicSong } from '../../api/subsonicTypes';
import { formatTime } from '../../utils/componentHelpers/nowPlayingHelpers';
import { formatTrackTime } from '../../utils/format/formatDuration';
interface TopSongsCardProps {
artistName: string;
@@ -44,7 +44,7 @@ const TopSongsCard = memo(function TopSongsCard({ artistName, artistId, songs, c
<span className="np-dash-top-title truncate">{s.title}</span>
{s.album && <span className="np-dash-top-sub truncate">{s.album}</span>}
</div>
<span className="np-dash-top-dur">{formatTime(s.duration)}</span>
<span className="np-dash-top-dur">{formatTrackTime(s.duration)}</span>
<Play size={14} className="np-dash-top-play" />
</div>
);
+4 -4
View File
@@ -1,6 +1,6 @@
import { memo, useEffect, useRef } from 'react';
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../../store/playbackProgress';
import { formatTime } from '../../utils/componentHelpers/playerBarHelpers';
import { 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. */
@@ -8,10 +8,10 @@ export const PlaybackTime = memo(function PlaybackTime({ className }: { classNam
const spanRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
if (spanRef.current) {
spanRef.current.textContent = formatTime(getPlaybackProgressSnapshot().currentTime);
spanRef.current.textContent = formatTrackTime(getPlaybackProgressSnapshot().currentTime);
}
return subscribePlaybackProgress(state => {
if (spanRef.current) spanRef.current.textContent = formatTime(state.currentTime);
if (spanRef.current) spanRef.current.textContent = formatTrackTime(state.currentTime);
});
}, []);
return <span className={className} ref={spanRef} />;
@@ -25,7 +25,7 @@ export const RemainingTime = memo(function RemainingTime({ duration, className }
const updateRemaining = () => {
if (spanRef.current) {
const remaining = Math.max(0, duration - getPlaybackProgressSnapshot().currentTime);
spanRef.current.textContent = `-${formatTime(remaining)}`;
spanRef.current.textContent = `-${formatTrackTime(remaining)}`;
}
};
updateRemaining();
@@ -2,7 +2,7 @@ import { ArrowLeftRight } from 'lucide-react';
import type { TFunction } from 'i18next';
import type { RadioMetadata } from '../../hooks/useRadioMetadata';
import { useThemeStore } from '../../store/themeStore';
import { formatTime } from '../../utils/componentHelpers/playerBarHelpers';
import { formatTrackTime } from '../../utils/format/formatDuration';
import WaveformSeek from '../WaveformSeek';
import { PlaybackTime, RemainingTime } from './PlaybackClock';
@@ -27,7 +27,7 @@ export function PlayerSeekbarSection({
<>
{radioMeta.source === 'azuracast' && radioMeta.elapsed != null && radioMeta.duration != null && radioMeta.duration > 0 ? (
<>
<span className="player-time">{formatTime(radioMeta.elapsed)}</span>
<span className="player-time">{formatTrackTime(radioMeta.elapsed)}</span>
<div className="player-waveform-wrap">
<div className="radio-progress-bar">
<div
@@ -36,7 +36,7 @@ export function PlayerSeekbarSection({
/>
</div>
</div>
<span className="player-time">{formatTime(radioMeta.duration)}</span>
<span className="player-time">{formatTrackTime(radioMeta.duration)}</span>
</>
) : (
<>
@@ -65,7 +65,7 @@ export function PlayerSeekbarSection({
}}
data-tooltip={localShowRemaining ? t('player.showDuration') : t('player.showRemainingTime')}
>
{localShowRemaining ? <RemainingTime duration={duration} /> : formatTime(duration)}
{localShowRemaining ? <RemainingTime duration={duration} /> : formatTrackTime(duration)}
<ArrowLeftRight size={10} style={{ marginLeft: 4, opacity: 0.6 }} />
</span>
</>
@@ -5,7 +5,7 @@ import { buildCoverArtUrl, coverArtCacheKey } from '../../api/subsonicStreamUrl'
import type { SubsonicSong } from '../../api/subsonicTypes';
import { usePlayerStore } from '../../store/playerStore';
import { songToTrack } from '../../utils/playback/songToTrack';
import { formatDuration } from '../../utils/componentHelpers/playlistDetailHelpers';
import { formatTrackTime } from '../../utils/format/formatDuration';
import CachedImage from '../CachedImage';
import { AddToPlaylistSubmenu } from '../ContextMenu';
@@ -132,7 +132,7 @@ export default function PlaylistSongSearchPanel({
<span className="playlist-search-title">{song.title}</span>
<span className="playlist-search-artist">{song.artist} · <span className="playlist-search-album">{song.album}</span></span>
</div>
<span className="playlist-search-duration">{formatDuration(song.duration ?? 0)}</span>
<span className="playlist-search-duration">{formatTrackTime(song.duration ?? 0)}</span>
</div>
);
})}
@@ -8,7 +8,8 @@ import { usePlayerStore } from '../../store/playerStore';
import { usePreviewStore } from '../../store/previewStore';
import { useThemeStore } from '../../store/themeStore';
import { songToTrack } from '../../utils/playback/songToTrack';
import { codecLabel, formatDuration } from '../../utils/componentHelpers/playlistDetailHelpers';
import { codecLabel } from '../../utils/componentHelpers/playlistDetailHelpers';
import { formatTrackTime } from '../../utils/format/formatDuration';
const PL_CENTERED = new Set(['favorite', 'rating', 'duration']);
@@ -156,7 +157,7 @@ export default function PlaylistSuggestions({
);
case 'favorite': return <div key="favorite" />;
case 'rating': return <div key="rating" />;
case 'duration': return <div key="duration" className="track-duration">{formatDuration(song.duration ?? 0)}</div>;
case 'duration': return <div key="duration" className="track-duration">{formatTrackTime(song.duration ?? 0)}</div>;
case 'format': return (
<div key="format" className="track-meta">
{(song.suffix || (showBitrate && song.bitRate)) && <span className="track-codec">{codecLabel(song, showBitrate)}</span>}
@@ -13,7 +13,8 @@ import { useThemeStore } from '../../store/themeStore';
import { useDragDrop } from '../../contexts/DragDropContext';
import { useOrbitSongRowBehavior } from '../../hooks/useOrbitSongRowBehavior';
import { songToTrack } from '../../utils/playback/songToTrack';
import { codecLabel, formatDuration } from '../../utils/componentHelpers/playlistDetailHelpers';
import { codecLabel } from '../../utils/componentHelpers/playlistDetailHelpers';
import { formatTrackTime } from '../../utils/format/formatDuration';
import type { PlaylistSortKey, PlaylistSortDir } from '../../utils/playlist/playlistDisplayedSongs';
import StarRating from '../StarRating';
import { AddToPlaylistSubmenu } from '../ContextMenu';
@@ -405,7 +406,7 @@ export default function PlaylistTracklist({
</div>
);
case 'rating': return <StarRating key="rating" value={ratings[song.id] ?? userRatingOverrides[song.id] ?? song.userRating ?? 0} onChange={r => handleRate(song.id, r)} />;
case 'duration': return <div key="duration" className="track-duration">{formatDuration(song.duration ?? 0)}</div>;
case 'duration': return <div key="duration" className="track-duration">{formatTrackTime(song.duration ?? 0)}</div>;
case 'format': return (
<div key="format" className="track-meta">
{(song.suffix || (showBitrate && song.bitRate)) && <span className="track-codec">{codecLabel(song, showBitrate)}</span>}
+3 -8
View File
@@ -4,6 +4,7 @@ import type { TFunction } from 'i18next';
import { usePlayerStore } from '../../store/playerStore';
import type { Track } from '../../store/playerStoreTypes';
import type { DurationMode } from '../../utils/componentHelpers/queuePanelHelpers';
import { formatLongDuration } from '../../utils/format/formatDuration';
interface Props {
queue: Track[];
@@ -34,12 +35,6 @@ export function QueueHeader({
const remainingSecs = Math.max(0, (queue[queueIndex]?.duration ?? 0) - currentTime + futureTracksDuration);
const fmt = (secs: number) => {
const h = Math.floor(secs / 3600);
const m = Math.floor((secs % 3600) / 60);
const s = secs % 60;
return h > 0 ? `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}` : `${m}:${s.toString().padStart(2, "0")}`;
};
const fmtEta = (secs: number) => {
const finishTime = new Date(Date.now() + secs * 1000);
return new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(finishTime);
@@ -47,8 +42,8 @@ export function QueueHeader({
let dur: string | null = null;
if (queue.length > 0) {
if (durationMode === 'total') dur = fmt(Math.floor(totalSecs));
else if (durationMode === 'remaining') dur = `-${fmt(Math.floor(remainingSecs))}`;
if (durationMode === 'total') dur = formatLongDuration(Math.floor(totalSecs));
else if (durationMode === 'remaining') dur = `-${formatLongDuration(Math.floor(remainingSecs))}`;
else dur = fmtEta(remainingSecs);
}
+2 -2
View File
@@ -5,7 +5,7 @@ import OverlayScrollArea from '../OverlayScrollArea';
import { usePlayerStore } from '../../store/playerStore';
import { useLuckyMixStore } from '../../store/luckyMixStore';
import type { Track, PlayerState } from '../../store/playerStoreTypes';
import { formatTime } from '../../utils/componentHelpers/queuePanelHelpers';
import { formatTrackTime } from '../../utils/format/formatDuration';
type StartDrag = (
payload: { data: string; label: string },
@@ -125,7 +125,7 @@ export function QueueList({
})()}
</div>
<div className="queue-item-duration">
{formatTime(track.duration)}
{formatTrackTime(track.duration)}
</div>
</div>
{luckyRolling && isPlaying && (