mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
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:
committed by
GitHub
parent
7a7a9f5e6b
commit
5231169a71
@@ -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 && (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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,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,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>
|
||||
);
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user