refactor(dedup): consolidate byte / sanitize / clock / album-duration helpers (Phase L, part 2) (#692)

Findings 5-8 of the dedup audit:

- F5 byte formatters: appUpdaterHelpers.fmtBytes + ZipDownloadOverlay
  .formatMB route through the existing formatBytes; a new formatMb
  (always-MB) backs playlistDetailHelpers.formatSize, AlbumHeader and
  the 4 inline DeviceSyncPreSyncModal expressions. SongInfoModal.format
  Size is intentionally left — it uses decimal (1e6) divisors, not 1024.
- F6 sanitizeHtml: extracted to utils/sanitizeHtml.ts; AlbumHeader,
  ComposerDetail and the (now-empty, deleted) artistDetailHelpers use it
  directly. nowPlayingHelpers keeps its own export but now delegates to
  the shared sanitiser and only adds its trailing-link strip on top.
- F7 album duration: BecauseYouLikeRail's formatAlbumDuration drops in
  favour of the shared formatHumanHoursMinutes. Behaviour note: total
  minutes now floor instead of round (<=1 min display difference,
  matches every other caller).
- F8 clock time: extracted to utils/format/formatClockTime.ts;
  PlaybackDelayModal + QueueHeader use it (toLocaleTimeString and
  Intl.DateTimeFormat produced identical output).

Behaviour preserved except the two explicitly noted divergences (F7
round->floor; F5 appUpdater/Zip now show GB above 1 GB instead of a
large MB number).
This commit is contained in:
Frank Stellmacher
2026-05-14 15:19:42 +02:00
committed by GitHub
parent 0153435787
commit 4b1dd3c29f
15 changed files with 50 additions and 105 deletions
@@ -2,6 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { AlertCircle, Loader2 } from 'lucide-react';
import type { SyncDelta } from '../../utils/deviceSync/runDeviceSyncExecution';
import { formatMb } from '../../utils/format/formatBytes';
interface Props {
preSyncOpen: boolean;
@@ -32,20 +33,20 @@ export default function DeviceSyncPreSyncModal({
<div className="device-sync-summary-stats" style={{ display: 'flex', flexDirection: 'column', gap: '8px', margin: '10px 0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0' }}>
<span>{t('deviceSync.filesToAdd')}</span>
<span className="color-success">+{syncDelta.addCount} ({(syncDelta.addBytes / 1_048_576).toFixed(1)} MB)</span>
<span className="color-success">+{syncDelta.addCount} ({formatMb(syncDelta.addBytes)})</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0' }}>
<span>{t('deviceSync.filesToDelete')}</span>
<span className="color-error">-{syncDelta.delCount} ({(syncDelta.delBytes / 1_048_576).toFixed(1)} MB)</span>
<span className="color-error">-{syncDelta.delCount} ({formatMb(syncDelta.delBytes)})</span>
</div>
<hr style={{ border: 'none', borderTop: '1px solid var(--border)', margin: '10px 0' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 'bold' }}>
<span>{t('deviceSync.netChange')}</span>
<span>{((syncDelta.addBytes - syncDelta.delBytes) / 1_048_576).toFixed(1)} MB</span>
<span>{formatMb(syncDelta.addBytes - syncDelta.delBytes)}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 'bold', color: syncDelta.addBytes > syncDelta.availableBytes + syncDelta.delBytes ? 'var(--danger)' : 'inherit', marginTop: '10px' }}>
<span>{t('deviceSync.availableSpace')}</span>
<span>{(syncDelta.availableBytes / 1_048_576).toFixed(1)} MB</span>
<span>{formatMb(syncDelta.availableBytes)}</span>
</div>
{syncDelta.addBytes > syncDelta.availableBytes + syncDelta.delBytes && (
<div className="sync-warning error" style={{ background: 'color-mix(in srgb, var(--danger) 15%, transparent)', padding: '10px', borderRadius: 'var(--radius-md)', marginTop: '15px', display: 'flex', gap: '10px', color: 'var(--danger)', alignItems: 'flex-start' }}>