songs.tsx 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import React, { CSSProperties, useContext, useMemo, useRef } from 'react';
  2. import AutoSizer from 'react-virtualized-auto-sizer';
  3. import { FixedSizeList as List } from 'react-window';
  4. import { StateContext } from '../../../../context/state';
  5. import { Song } from '../../../../types';
  6. import { namedMemo } from '../../../../utils/component';
  7. import { CmusUIStateContext } from '../reducer';
  8. import { getFilteredSongs } from '../selectors';
  9. import { NoWrapFill } from '../styled/layout';
  10. import { AsciiSpinner } from '../styled/spinner';
  11. import { getSongScrollIndex, lineHeight, useAutoJumpyScroll } from '../utils/scroll';
  12. import * as Styled from './songs.styles';
  13. type Props = {
  14. active: boolean;
  15. };
  16. type SongData = {
  17. song: Song;
  18. active: boolean;
  19. parentActive: boolean;
  20. highlight: boolean;
  21. };
  22. const itemKey = (index: number, data: SongData[]): number => data[index].song.id;
  23. const Row = namedMemo<{ index: number; data: SongData[]; style: CSSProperties }>(
  24. 'Song',
  25. ({ index, data, style }) => {
  26. const { song, active, parentActive, highlight } = data[index];
  27. return (
  28. <Styled.Song active={active} parentActive={parentActive} style={style} highlight={highlight}>
  29. <NoWrapFill>
  30. {song.track} - {song.title || 'Untitled Track'}
  31. </NoWrapFill>
  32. </Styled.Song>
  33. );
  34. },
  35. );
  36. export const Songs: React.FC<Props> = ({ active: parentActive }) => {
  37. const globalState = useContext(StateContext);
  38. const { songId: playingSongId } = globalState.player;
  39. const state = useContext(CmusUIStateContext);
  40. const { activeArtist, activeSongId } = state.library;
  41. const filteredSongs = getFilteredSongs(state);
  42. const itemData = useMemo<SongData[]>(
  43. () =>
  44. filteredSongs.map<SongData>((song) => ({
  45. song,
  46. active: song.id === activeSongId,
  47. parentActive,
  48. highlight: song.id === playingSongId,
  49. })),
  50. [parentActive, activeSongId, playingSongId, filteredSongs],
  51. );
  52. const windowRef = useRef<HTMLDivElement>(null);
  53. const scrollIndex = getSongScrollIndex(filteredSongs, activeSongId);
  54. useAutoJumpyScroll(windowRef, scrollIndex);
  55. if (activeArtist !== null && !(activeArtist in state.artistSongs)) {
  56. return (
  57. <Styled.Container>
  58. <AsciiSpinner />
  59. </Styled.Container>
  60. );
  61. }
  62. return (
  63. <Styled.Container>
  64. <AutoSizer>
  65. {({ height, width }): React.ReactElement => (
  66. <List
  67. outerRef={windowRef}
  68. height={height}
  69. width={width}
  70. itemCount={itemData.length}
  71. itemSize={lineHeight}
  72. itemKey={itemKey}
  73. itemData={itemData}
  74. >
  75. {Row}
  76. </List>
  77. )}
  78. </AutoSizer>
  79. </Styled.Container>
  80. );
  81. };