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).
111 of 122 top-level src/utils/ files move into 16 topic folders (audio,
cache, cover, share, server, playback, playlist, deviceSync, waveform,
mix, format, export, changelog, ui, perf, componentHelpers). True
singletons with no cluster stay at the utils/ root.
Pure file-move: a path-aware codemod rewrote 539 relative-import
specifiers across 275 files; no logic touched. The hot-path coverage
gate list (.github/frontend-hot-path-files.txt) is updated to the new
paths for the 11 gated utils files — a mechanical consequence of the
move, not a CI change. tsc is green.
Two-cut cluster pulling the main layout columns out of
DeviceSync.tsx. 511 → 233 LOC (−278). DeviceSync is now mostly
glue: state hooks, hook calls, action wrappers, and a flat tree of
five layout components.
DeviceSyncBrowserPanel — left column. Owns the tabs row
(playlists / albums / artists with icons), the search input with
the "Live search" badge on the albums tab, and the result list:
loading spinner, "Random albums" section label, playlist /
album / artist rows with their BrowserRow leaf component, and the
expand-an-artist tree (loading state, chevron, child album rows
with indent). filteredPlaylists / filteredArtists memos move into
the panel since only the row mapping consumes them.
DeviceSyncDevicePanel — right column. Owns the header (title +
scanning spinner + sync action button with three label variants
+ "Delete from device" button), the status badges row
(synced / pending / deletion), the source list with checkbox /
type / status icon / per-row action (mark-for-deletion /
remove-source / undo-deletion), and the bottom progress strip
(running / cancelled / done) with their dismiss / cancel
buttons. invoke('cancel_device_sync') stays in the panel since
it's a panel-local action.
DeviceSync drops the now-unused invoke / BrowserRow / useMemo
imports (filteredPlaylists/Artists moved into the panel). Pure
code move otherwise.
Three-cut cluster pulling the chrome out of DeviceSync.tsx. 739 →
501 LOC (−238).
DeviceSyncHeader — title row, fixed-scheme info block (with the
"Reorganize existing files…" migrate button), and the drive picker
row (manual folder picker, refresh, CustomSelect over detected
drives or no-drives fallback, drive metadata line).
DeviceSyncPreSyncModal — the modal that opens before sync execution:
loading spinner while calculate_sync_payload runs, then the
delta-stats grid (add count + bytes, delete count + bytes, net
change, available space) with the space-warning when add exceeds
available + del, plus the cancel / proceed footer.
DeviceSyncMigrationModal — the migrate-existing-files modal with
its five-phase state machine (loading / nothing / preview /
executing / done): preview lists rename count + unchanged count
+ collision warning + old-template note; done shows ok / failed
counts + a collapsible error list capped at 50 entries.
DeviceSync drops the inline JSX + the now-unused HardDriveUpload /
FolderOpen / Usb / RefreshCw / Loader2 (partially) icon imports
that only the header used. Pure code move otherwise.
Four-cut cluster opening the DeviceSync refactor. 1289 → 1186 LOC
(−103).
deviceSyncHelpers — uuid, formatBytes, trackToSyncInfo (with the
albumArtist-fallback-to-artist logic and the optional playlist
context the Rust sync command consumes), plus the SourceTab /
SyncStatus / RemovableDrive / SyncTrackMaybePlaylist types.
deviceSyncLegacyTemplate — sanitizeComponent (matches Rust's
sanitize_path_component) + OldTemplateTrack + applyLegacyTemplate.
Lives apart from the general helpers because it's only used by the
migration-preview flow and pulls in IS_WINDOWS for path separator.
fetchTracksForSource — single async helper that loads the songs for
a DeviceSyncSource (playlist / album / artist). Artist sources fan
out into parallel getAlbum requests (Navidrome handles them
concurrently; the old sequential loop was a ~7 s blocker on
50-album artists).
BrowserRow — small button row component used by the source-picker
list (playlists / albums / artists tabs).
DeviceSync drops the inline definitions plus the direct getAlbum /
getPlaylist / getArtist imports that only fetchTracksForSource
needed. Pure code move — no behaviour change.