diff --git a/packages/core/src/plugins/meta/spotify.test.ts b/packages/core/src/plugins/meta/spotify.test.ts index 21ebe84e04..d4c788e0f6 100644 --- a/packages/core/src/plugins/meta/spotify.test.ts +++ b/packages/core/src/plugins/meta/spotify.test.ts @@ -355,6 +355,7 @@ describe('SpotifyMetaProvider', () => { name: 'test track', artists: [{ name: 'test artist' }], duration_ms: 1000, + disc_number: 0, type: 'track' }] }, @@ -379,6 +380,7 @@ describe('SpotifyMetaProvider', () => { title: 'test track', artist: 'test artist', duration: 1, + discNumber: 0, thumbnail: 'thumbnail.jpg' }] }); diff --git a/packages/core/src/plugins/meta/spotify.ts b/packages/core/src/plugins/meta/spotify.ts index 478485f5db..1ba2c67146 100644 --- a/packages/core/src/plugins/meta/spotify.ts +++ b/packages/core/src/plugins/meta/spotify.ts @@ -1,3 +1,4 @@ +import spotify from '../../helpers/playlist/spotify'; import { SpotifyArtist, SpotifyClientProvider, SpotifyImage, SpotifySimplifiedAlbum, SpotifyTrack, getImageSet } from '../../rest/Spotify'; import Track from '../../structs/Track'; import MetaProvider from '../metaProvider'; @@ -13,7 +14,7 @@ export class SpotifyMetaProvider extends MetaProvider { this.image = null; this.isDefault = true; } - + async searchForArtists(query: string, limit=10): Promise { const client = await SpotifyClientProvider.get(); const results = await client.searchArtists(query, limit); @@ -67,6 +68,7 @@ export class SpotifyMetaProvider extends MetaProvider { id: spotifyTrack.id, title: spotifyTrack.name, artist: spotifyTrack.artists[0].name, + discNumber: spotifyTrack.disc_number, source: SearchResultsSource.Spotify, thumb }; @@ -169,6 +171,7 @@ export class SpotifyMetaProvider extends MetaProvider { title: track.name, artist: result.artists[0].name, duration: track.duration_ms/1000, + discNumber: track.disc_number, position: track.track_number, thumbnail: thumb })) @@ -181,6 +184,6 @@ export class SpotifyMetaProvider extends MetaProvider { return this.fetchAlbumDetails(result.id, albumType); } - + } diff --git a/packages/core/src/plugins/plugins.types.ts b/packages/core/src/plugins/plugins.types.ts index 955528f8ac..323595d2a5 100644 --- a/packages/core/src/plugins/plugins.types.ts +++ b/packages/core/src/plugins/plugins.types.ts @@ -62,6 +62,7 @@ export type SearchResultsTrack = { artist: string; source: SearchResultsSource; thumb?: string; + discNumber?: number | string; } export type ArtistTopTrack = { diff --git a/packages/core/src/rest/Spotify.ts b/packages/core/src/rest/Spotify.ts index 6d3c1a448f..230bc0d5cb 100644 --- a/packages/core/src/rest/Spotify.ts +++ b/packages/core/src/rest/Spotify.ts @@ -65,6 +65,7 @@ export type SpotifyTrack = { popularity: number; track_number: number; duration_ms: number; + disc_number: number; type: 'track'; } diff --git a/packages/core/src/structs/Track.ts b/packages/core/src/structs/Track.ts index c8d14adce5..7544682e56 100644 --- a/packages/core/src/structs/Track.ts +++ b/packages/core/src/structs/Track.ts @@ -15,8 +15,9 @@ export default class Track { title: string; name?: string; duration: string | number; - + position?: string | number; + discNumber?: string | number; playcount?: string | number; thumbnail?: string; extraArtists?: string[]; @@ -35,6 +36,7 @@ export default class Track { this.duration = data.duration; this.position = data.position; this.thumbnail = data.thumbnail; + this.discNumber = data.discNumber; } addSearchResultData(data: SearchResultsTrack): void { @@ -42,6 +44,7 @@ export default class Track { this.artist = data.artist; this.title = data.title; this.name = data.title; + this.discNumber = data.discNumber; } static fromSearchResultData(data: SearchResultsTrack): Track { diff --git a/packages/ui/lib/components/GridTrackTable/index.tsx b/packages/ui/lib/components/GridTrackTable/index.tsx index eafc22967e..ebd600bb02 100644 --- a/packages/ui/lib/components/GridTrackTable/index.tsx +++ b/packages/ui/lib/components/GridTrackTable/index.tsx @@ -3,18 +3,18 @@ import React, { useMemo, useState } from 'react'; import { DragDropContext, DragDropContextProps, Droppable } from 'react-beautiful-dnd'; import { FixedSizeList } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; - + import { TrackTableColumn, TrackTableExtraProps, TrackTableHeaders, TrackTableSettings, TrackTableStrings } from '../TrackTable/types'; import { TextHeader } from './Headers/TextHeader'; import { TextCell } from './Cells/TextCell'; import { Track } from '../../types'; import { getTrackThumbnail } from '../TrackRow'; - + import styles from './styles.scss'; import artPlaceholder from '../../../resources/media/art_placeholder.png'; import { ThumbnailCell } from './Cells/ThumbnailCell'; import { GridTrackTableRow } from './GridTrackTableRow'; -import { isNumber, isString } from 'lodash'; +import { groupBy, isNumber, isString } from 'lodash'; import { SelectionCell } from './Cells/SelectionCell'; import { SelectionHeader } from './Headers/SelectionHeader'; import { formatDuration } from '../../utils'; @@ -25,7 +25,7 @@ import { FavoriteCell } from './Cells/FavoriteCell'; import { TitleCell } from './Cells/TitleCell'; import { Input } from 'semantic-ui-react'; import Button from '../Button'; - + export type GridTrackTableProps = { className?: string; tracks: T[]; @@ -36,28 +36,28 @@ export type GridTrackTableProps = { } & TrackTableHeaders & TrackTableSettings & TrackTableExtraProps; - + type ColumnWithWidth = Column & { columnWidth: string; }; type TrackTableColumnInstance = ColumnInstance & UseSortByColumnProps; type TrackTableHeaderGroup = HeaderGroup & UseSortByColumnProps; type TrackTableInstance = TableInstance & UseGlobalFiltersInstanceProps & UseSortByInstanceProps & UseRowSelectInstanceProps; type TrackTableState = TableState & UseSortByState; export type TrackTableRow = Row & UseRowSelectRowProps; - + export const GridTrackTable = ({ className, tracks, customColumns=[], isTrackFavorite, onDragEnd, - + positionHeader, thumbnailHeader, artistHeader, titleHeader, albumHeader, durationHeader, - + displayHeaders=true, displayDeleteButton=true, displayPosition=true, @@ -69,11 +69,15 @@ export const GridTrackTable = ({ displayCustom=true, selectable=true, searchable=false, - + ...extraProps }: GridTrackTableProps) => { const shouldDisplayDuration = displayDuration && tracks.every(track => Boolean(track.duration)); - const columns = useMemo(() => [ + const groupedDisc = useMemo(() => groupBy(tracks, (track) => track.discNumber || 1), [tracks]); + + const [globalFilter, setGlobalFilterState] = useState(''); // Required, because useGlobalFilter does not provide a way to get the current filter value + + const generateColumns = () => [ selectable && { id: TrackTableColumn.Selection, Header: SelectionHeader, @@ -82,17 +86,12 @@ export const GridTrackTable = ({ }, displayPosition && { id: TrackTableColumn.Position, - Header: ({ column }) => } - header={positionHeader} - isCentered - />, + Header: ({ column }) => } header={positionHeader} isCentered />, accessor: (track: T) => track.position, Cell: PositionCell, enableSorting: true, columnWidth: '4em' - } as Column, + }, displayThumbnail && { id: TrackTableColumn.Thumbnail, Header: ({ column }) => , @@ -117,9 +116,7 @@ export const GridTrackTable = ({ displayArtist && { id: TrackTableColumn.Artist, Header: ({ column }) => , - accessor: (track: T) => isString(track.artist) - ? track.artist - : track.artist.name, + accessor: (track: T) => isString(track.artist) ? track.artist : track.artist.name, Cell: TextCell, enableSorting: true, columnWidth: '6em' @@ -131,19 +128,11 @@ export const GridTrackTable = ({ enableSorting: true, Cell: TextCell, columnWidth: '6em' - } as Column, + }, shouldDisplayDuration && { id: TrackTableColumn.Duration, Header: ({ column }) => , - accessor: (track: T) => { - if (isString(track.duration)) { - return track.duration; - } else if (isNumber(track.duration)) { - return formatDuration(track.duration); - } else { - return null; - } - }, + accessor: (track: T) => isString(track.duration) ? track.duration : formatDuration(track.duration), Cell: TextCell, columnWidth: '6em' }, @@ -152,134 +141,111 @@ export const GridTrackTable = ({ id: TrackTableColumn.Delete, Cell: DeleteCell, columnWidth: '3em' - } as Column - ].filter(Boolean), [displayDeleteButton, displayPosition, displayThumbnail, displayFavorite, isTrackFavorite, titleHeader, displayArtist, artistHeader, displayAlbum, albumHeader, shouldDisplayDuration, durationHeader, selectable, positionHeader, thumbnailHeader]); - - const data = useMemo(() => tracks, [tracks]); - - const initialState: Partial & UseSortByState> = { - sortBy: [{ id: TrackTableColumn.Position, desc: false }] - }; - const table = useTable({ columns, data, initialState }, useGlobalFilter, useSortBy, useRowSelect); - const [globalFilter, setGlobalFilterState] = useState(''); // Required, because useGlobalFilter does not provide a way to get the current filter value - - const { - getTableProps, - getTableBodyProps, - headerGroups, - rows, - prepareRow, - setGlobalFilter, - state: tableState, - selectedFlatRows - } = table as TrackTableInstance; - - const onFilterClick = () => { - setGlobalFilter(''); - setGlobalFilterState(''); - }; - - const gridTemplateColumns = columns.map((column: ColumnWithWidth) => column.columnWidth ?? '1fr').join(' '); - - // Disabled when there are selected rows, or when sorted by anything other than position - const isDragDisabled = !onDragEnd || selectedFlatRows.length > 0 || (tableState as TrackTableState).sortBy[0]?.id !== TrackTableColumn.Position; - - return
- { - searchable && -
- { - setGlobalFilter(e.target.value); - setGlobalFilterState(e.target.value); - }} - value={globalFilter} - /> -
} -
-
- {headerGroups.map(headerGroup => ( -
- { - headerGroup.headers.map((column: TrackTableHeaderGroup) => ( -
- {column.render('Header', extraProps)} -
)) - } + ].filter(Boolean); + + const renderTable = (data: T[], discNumber?: string) => { + const columns = useMemo(generateColumns, [displayDeleteButton, displayPosition, displayThumbnail, displayFavorite, isTrackFavorite, titleHeader, displayArtist, artistHeader, displayAlbum, albumHeader, shouldDisplayDuration, durationHeader, selectable, positionHeader, thumbnailHeader]); + + const initialState: Partial & UseSortByState> = { + sortBy: [{ id: TrackTableColumn.Position, desc: false }] + }; + + const table = useTable( + { columns, data, initialState }, + useGlobalFilter, useSortBy, useRowSelect + ); + + const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow, setGlobalFilter, state: tableState, selectedFlatRows } = table as TrackTableInstance; + const onFilterClick = () => { + setGlobalFilter(''); + setGlobalFilterState(''); + }; + const gridTemplateColumns = columns.map((col: ColumnWithWidth) => col.columnWidth ?? '1fr').join(' '); + + const isDragDisabled = !onDragEnd || selectedFlatRows.length > 0 || (tableState as TrackTableState).sortBy[0]?.id !== TrackTableColumn.Position; + + return ( +
+ {searchable && ( +
+ { + setGlobalFilter(e.target.value); + setGlobalFilterState(e.target.value); + }} + value={globalFilter} + /> +
+ )} +
+ {discNumber &&

Disc {discNumber}

} +
+ {headerGroups.map((headerGroup) => ( +
+ {headerGroup.headers.map((column: TrackTableHeaderGroup) => ( +
+ {column.render('Header', extraProps)} +
+ ))} +
+ ))}
- ))} + + + {(droppableProvided) => ( +
+ + {({ height, width }) => ( + [], + prepareRow: prepareRow as (row: Row) => void, + gridTemplateColumns, + isDragDisabled, + extraProps + }} + outerRef={droppableProvided.innerRef} + > + {GridTrackTableRow} + + )} + +
+ )} +
+
+
+
+ ); + }; + + if (Object.keys(groupedDisc).length < 2) { + return renderTable(tracks); + } else { + return ( +
+ {Object.entries(groupedDisc).map(([discNumber, discTracks]) => renderTable(discTracks, discNumber))}
- - - {(droppableProvided, droppableSnapshot) => ( -
- - {({ height, width }) => - [], - prepareRow: prepareRow as (row: Row) => void, - gridTemplateColumns, - isDragDisabled, - extraProps - }} - outerRef={droppableProvided.innerRef} - > - {GridTrackTableRow} - - } - -
- )} -
-
-
-
; + ); + } + }; - + diff --git a/packages/ui/lib/types/index.ts b/packages/ui/lib/types/index.ts index 49f753f909..c135f412bc 100644 --- a/packages/ui/lib/types/index.ts +++ b/packages/ui/lib/types/index.ts @@ -1,9 +1,9 @@ export type Track = { uuid?: string; loading?: boolean; - error?: boolean | { - message: string; - details: string + error?: boolean | { + message: string; + details: string }; local?: boolean; artist: { name: string } | string; @@ -12,6 +12,7 @@ export type Track = { album?: string; duration?: number | string; position?: number | string; + discNumber?: number | string; playcount?: number | string; thumbnail?: string; image?: { '#text'?: string }[]; diff --git a/packages/ui/test/__snapshots__/gridTrackTable.test.tsx.snap b/packages/ui/test/__snapshots__/gridTrackTable.test.tsx.snap index d759a8cf26..4b4c0c5cf8 100644 --- a/packages/ui/test/__snapshots__/gridTrackTable.test.tsx.snap +++ b/packages/ui/test/__snapshots__/gridTrackTable.test.tsx.snap @@ -736,3 +736,608 @@ exports[`(Snapshot) Grid track table - example data with all rows should render
`; + +exports[`(Snapshot) Grid track table - example data with multiple discs should render correctly 1`] = ` + +
+
+
+

+ Disc 0 +

+
+
+
+
+
+ +
+
+
+
+
+ Position +