refactor(album-track-list): I.2 — split AlbumTrackList.tsx 662 → 187 LOC across 7 files (#672)

* refactor(album-track-list): extract helpers + types

Pull formatDuration / codecLabel, the COLUMNS / CENTERED_COLS / SORTABLE_COLS
tables, ColKey / SortKey types, and the isSortable type guard into
utils/albumTrackListHelpers.ts. SortKey is re-exported from
AlbumTrackList.tsx so existing imports stay valid.

AlbumTrackList.tsx: 662 → 633 LOC.

* refactor(album-track-list): extract TrackRow subcomponent

Move the memoised tracklist row (~220 LOC including renderCell switch and
mouse handlers) into components/albumTrackList/TrackRow.tsx. It still
subscribes to its own selection + preview state via primitive selectors,
so per-row re-render scope is unchanged.

AlbumTrackList.tsx: 633 → 404 LOC.

* refactor(album-track-list): extract AlbumTrackListMobile subcomponent

Move the narrow-viewport branch (compact tracklist with disc separators
and no column grid) into components/albumTrackList/AlbumTrackListMobile.tsx.

AlbumTrackList.tsx: 404 → 376 LOC.

* refactor(album-track-list): extract TracklistColumnPicker subcomponent

The column visibility dropdown lives outside .tracklist to avoid the
overflow box clipping its menu — pull the wrapper + button + popover into
components/albumTrackList/TracklistColumnPicker.tsx.

AlbumTrackList.tsx: 376 → 347 LOC.

* refactor(album-track-list): extract TracklistHeaderRow subcomponent

The fixed header (sortable + resizable per-column with the bulk-select
toggle on the num cell) moves into
components/albumTrackList/TracklistHeaderRow.tsx, taking 85+ LOC of cell
rendering with it.

AlbumTrackList.tsx: 347 → 254 LOC.

* refactor(album-track-list): extract useAlbumTrackListSelection hook

Pull bulk-selection state (selectedIds-size subscription, shift-range
toggle, click-outside-clear, song-list-change clear) and the drag-start
dispatcher (single vs multi-song drag) into
hooks/useAlbumTrackListSelection.ts.

AlbumTrackList.tsx: 254 → 187 LOC.
This commit is contained in:
Frank Stellmacher
2026-05-14 00:01:36 +02:00
committed by GitHub
parent b591a1cb5f
commit f14c8f21e6
7 changed files with 726 additions and 522 deletions
@@ -0,0 +1,82 @@
import React from 'react';
import { AudioLines } from 'lucide-react';
import type { SubsonicSong } from '../../api/subsonicTypes';
import type { Track } from '../../store/playerStoreTypes';
import { songToTrack } from '../../utils/songToTrack';
import { formatDuration } from '../../utils/albumTrackListHelpers';
interface Props {
discNums: number[];
discs: Map<number, SubsonicSong[]>;
isMultiDisc: boolean;
currentTrackId: string | null;
isPlaying: boolean;
contextMenuSongId: string | null;
setContextMenuSongId: (id: string | null) => void;
onPlaySong: (song: SubsonicSong) => void;
onContextMenu: (
x: number,
y: number,
track: Track,
type: 'song' | 'album' | 'artist' | 'queue-item' | 'album-song',
) => void;
}
/**
* Compact tracklist for narrow viewports. Drops the column grid + column
* picker + drag selection entirely — just a one-line row per song with
* disc group separators. Play on tap, context menu on long-press / right
* click.
*/
export function AlbumTrackListMobile({
discNums,
discs,
isMultiDisc,
currentTrackId,
isPlaying,
contextMenuSongId,
setContextMenuSongId,
onPlaySong,
onContextMenu,
}: Props) {
return (
<div className="tracklist-mobile">
{discNums.map(discNum => (
<div key={discNum}>
{isMultiDisc && (
<div className="disc-header">
<span className="disc-icon">💿</span> CD {discNum}
</div>
)}
{discs.get(discNum)!.map(song => {
const isActive = currentTrackId === song.id;
return (
<div
key={song.id}
className={`tracklist-mobile-row${isActive ? ' active' : ''}${contextMenuSongId === song.id ? ' context-active' : ''}`}
onClick={() => onPlaySong(song)}
onContextMenu={e => {
e.preventDefault();
setContextMenuSongId(song.id);
onContextMenu(e.clientX, e.clientY, songToTrack(song), 'album-song');
}}
>
<div className="tracklist-mobile-main">
{isActive && isPlaying ? (
<span className="tracklist-mobile-eq">
<AudioLines className="eq-bars" size={14} />
</span>
) : (
<span className="tracklist-mobile-num">{song.track ?? ''}</span>
)}
<span className="tracklist-mobile-title">{song.title}</span>
</div>
<span className="tracklist-mobile-duration">{formatDuration(song.duration)}</span>
</div>
);
})}
</div>
))}
</div>
);
}