|
@@ -1,6 +1,7 @@
|
|
|
-import React, { CSSProperties, useContext, useMemo, useRef } from 'react';
|
|
|
|
|
|
|
+import groupBy from 'lodash/groupBy';
|
|
|
|
|
+import React, { CSSProperties, useCallback, useContext, useMemo, useRef } from 'react';
|
|
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
|
|
-import { FixedSizeList as List } from 'react-window';
|
|
|
|
|
|
|
+import { VariableSizeList as List } from 'react-window';
|
|
|
import { StateContext } from '../../../../context/state';
|
|
import { StateContext } from '../../../../context/state';
|
|
|
|
|
|
|
|
import { Song } from '../../../../types';
|
|
import { Song } from '../../../../types';
|
|
@@ -24,12 +25,28 @@ type SongData = {
|
|
|
highlight: boolean;
|
|
highlight: boolean;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const itemKey = (index: number, data: SongData[]): number => data[index].song.id;
|
|
|
|
|
|
|
+type Separator = {
|
|
|
|
|
+ album: string;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+type ItemData = (SongData | Separator) & { id: number };
|
|
|
|
|
+
|
|
|
|
|
+const isSeparator = (item: ItemData | Separator): item is Separator => !Reflect.has(item, 'song');
|
|
|
|
|
+
|
|
|
|
|
+const itemKey = (index: number, data: ItemData[]): number => data[index].id;
|
|
|
|
|
|
|
|
-const Row = namedMemo<{ index: number; data: SongData[]; style: CSSProperties }>(
|
|
|
|
|
|
|
+const Row = namedMemo<{ index: number; data: ItemData[]; style: CSSProperties }>(
|
|
|
'Song',
|
|
'Song',
|
|
|
({ index, data, style }) => {
|
|
({ index, data, style }) => {
|
|
|
- const { song, active, parentActive, highlight } = data[index];
|
|
|
|
|
|
|
+ const item = data[index];
|
|
|
|
|
+ if (isSeparator(item)) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Styled.Separator style={style}>
|
|
|
|
|
+ <Styled.SeparatorText>{item.album || 'Unknown Album'}</Styled.SeparatorText>
|
|
|
|
|
+ </Styled.Separator>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ const { song, active, parentActive, highlight } = item;
|
|
|
return (
|
|
return (
|
|
|
<Styled.Song active={active} parentActive={parentActive} style={style} highlight={highlight}>
|
|
<Styled.Song active={active} parentActive={parentActive} style={style} highlight={highlight}>
|
|
|
<NoWrapFill>
|
|
<NoWrapFill>
|
|
@@ -46,19 +63,32 @@ export const Songs: React.FC<Props> = ({ active: parentActive }) => {
|
|
|
const { songId: playingSongId } = globalState.player;
|
|
const { songId: playingSongId } = globalState.player;
|
|
|
|
|
|
|
|
const state = useContext(CmusUIStateContext);
|
|
const state = useContext(CmusUIStateContext);
|
|
|
- const { activeArtist, activeSongId } = state.library;
|
|
|
|
|
|
|
+ const { activeArtist, activeAlbum, activeSongId } = state.library;
|
|
|
|
|
|
|
|
const filteredSongs = getFilteredSongs(state);
|
|
const filteredSongs = getFilteredSongs(state);
|
|
|
|
|
|
|
|
- const itemData = useMemo<SongData[]>(
|
|
|
|
|
- () =>
|
|
|
|
|
- filteredSongs.map<SongData>((song) => ({
|
|
|
|
|
- song,
|
|
|
|
|
- active: song.id === activeSongId,
|
|
|
|
|
- parentActive,
|
|
|
|
|
- highlight: song.id === playingSongId,
|
|
|
|
|
- })),
|
|
|
|
|
- [parentActive, activeSongId, playingSongId, filteredSongs],
|
|
|
|
|
|
|
+ const itemData = useMemo<ItemData[]>(() => {
|
|
|
|
|
+ const allSongs = filteredSongs.map<SongData & { id: number }>((song) => ({
|
|
|
|
|
+ id: song.id,
|
|
|
|
|
+ song,
|
|
|
|
|
+ active: song.id === activeSongId,
|
|
|
|
|
+ parentActive,
|
|
|
|
|
+ highlight: song.id === playingSongId,
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ if (activeAlbum !== null) {
|
|
|
|
|
+ return allSongs;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return Object.entries(groupBy(allSongs, ({ song }) => song.album)).reduce<ItemData[]>(
|
|
|
|
|
+ (last, [album, group], index) => [...last, { id: -index, album }, ...group],
|
|
|
|
|
+ [],
|
|
|
|
|
+ );
|
|
|
|
|
+ }, [parentActive, activeSongId, playingSongId, filteredSongs, activeAlbum]);
|
|
|
|
|
+
|
|
|
|
|
+ const getItemSize = useCallback(
|
|
|
|
|
+ (index: number): number => lineHeight * (isSeparator(itemData[index]) ? 2 : 1),
|
|
|
|
|
+ [itemData],
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
const windowRef = useRef<HTMLDivElement>(null);
|
|
const windowRef = useRef<HTMLDivElement>(null);
|
|
@@ -79,11 +109,12 @@ export const Songs: React.FC<Props> = ({ active: parentActive }) => {
|
|
|
<AutoSizer>
|
|
<AutoSizer>
|
|
|
{({ height, width }): React.ReactElement => (
|
|
{({ height, width }): React.ReactElement => (
|
|
|
<List
|
|
<List
|
|
|
|
|
+ key={`${activeArtist}-${activeAlbum}`}
|
|
|
outerRef={windowRef}
|
|
outerRef={windowRef}
|
|
|
height={height}
|
|
height={height}
|
|
|
width={width}
|
|
width={width}
|
|
|
itemCount={itemData.length}
|
|
itemCount={itemData.length}
|
|
|
- itemSize={lineHeight}
|
|
|
|
|
|
|
+ itemSize={getItemSize}
|
|
|
itemKey={itemKey}
|
|
itemKey={itemKey}
|
|
|
itemData={itemData}
|
|
itemData={itemData}
|
|
|
>
|
|
>
|