artists.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  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 { namedMemo } from '../../../../utils/component';
  5. import { CmusUIStateContext } from '../reducer';
  6. import { NoWrapFill } from '../styled/layout';
  7. import { SpinnerOrEmpty } from '../styled/spinner';
  8. import { getArtistAlbumScrollIndex, lineHeight, useAutoJumpyScroll } from '../utils/scroll';
  9. import * as Styled from './artists.styles';
  10. export type Props = {
  11. active: boolean;
  12. currentArtist: string | null;
  13. };
  14. type ArtistData = {
  15. id: string;
  16. artist: string;
  17. loading: boolean;
  18. active: boolean;
  19. parentActive: boolean;
  20. highlight: boolean;
  21. };
  22. type AlbumData = {
  23. id: string;
  24. album: string;
  25. active: boolean;
  26. parentActive: boolean;
  27. };
  28. type RowData = ArtistData | AlbumData;
  29. const isArtist = (data: RowData): data is ArtistData => Reflect.has(data, 'artist');
  30. const itemKey = (index: number, data: RowData[]): string => data[index].id;
  31. const Artist = namedMemo<{ row: ArtistData; style: CSSProperties }>(
  32. 'Artist',
  33. ({ row: { artist, loading, active, parentActive, highlight }, style }) => (
  34. <Styled.ArtistTitle
  35. active={active}
  36. parentActive={parentActive}
  37. style={style}
  38. highlight={highlight}
  39. >
  40. <SpinnerOrEmpty loading={loading} />
  41. <NoWrapFill>{artist || 'Unknown Artist'}</NoWrapFill>
  42. </Styled.ArtistTitle>
  43. ),
  44. );
  45. const Album = namedMemo<{ row: AlbumData; style: CSSProperties }>(
  46. 'Album',
  47. ({ row: { album, active, parentActive }, style }) => (
  48. <Styled.AlbumTitle active={active} parentActive={parentActive} style={style}>
  49. <NoWrapFill>{album || 'Unknown Album'}</NoWrapFill>
  50. </Styled.AlbumTitle>
  51. ),
  52. );
  53. const Row = namedMemo<{ index: number; data: RowData[]; style: CSSProperties }>(
  54. 'ArtistListRow',
  55. ({ index, data, style }) => {
  56. const row = data[index];
  57. if (isArtist(row)) {
  58. return <Artist row={row} style={style} />;
  59. }
  60. return <Album row={row} style={style} />;
  61. },
  62. );
  63. export const Artists: React.FC<Props> = ({ active: parentActive, currentArtist }) => {
  64. const {
  65. artists,
  66. artistAlbums,
  67. library: { activeArtist, activeAlbum, expandedArtists },
  68. } = useContext(CmusUIStateContext);
  69. const itemData = useMemo<RowData[]>(
  70. () =>
  71. artists.reduce<RowData[]>((last, artist) => {
  72. const expanded = expandedArtists.includes(artist);
  73. const artistRow: ArtistData = {
  74. id: artist,
  75. artist,
  76. loading: !(artist in artistAlbums) && expandedArtists.includes(artist),
  77. active: activeArtist === artist && activeAlbum === null,
  78. parentActive,
  79. highlight: currentArtist === artist,
  80. };
  81. if (!expanded) {
  82. return [...last, artistRow];
  83. }
  84. return [
  85. ...last,
  86. artistRow,
  87. ...(artistAlbums[artist] ?? []).map<AlbumData>((album) => ({
  88. id: `${artist}-${album}`,
  89. album,
  90. active: activeArtist === artist && activeAlbum === album,
  91. parentActive: parentActive && activeArtist === artist && activeAlbum === album,
  92. })),
  93. ];
  94. }, []),
  95. [
  96. parentActive,
  97. artists,
  98. artistAlbums,
  99. activeArtist,
  100. activeAlbum,
  101. expandedArtists,
  102. currentArtist,
  103. ],
  104. );
  105. const windowRef = useRef<HTMLDivElement>(null);
  106. const scrollIndex = getArtistAlbumScrollIndex(
  107. artists,
  108. artistAlbums,
  109. activeArtist,
  110. activeAlbum,
  111. expandedArtists,
  112. );
  113. useAutoJumpyScroll(windowRef, scrollIndex);
  114. return (
  115. <Styled.Container>
  116. <AutoSizer>
  117. {({ height, width }): React.ReactElement => (
  118. <List
  119. outerRef={windowRef}
  120. height={height}
  121. width={width}
  122. itemCount={itemData.length}
  123. itemSize={lineHeight}
  124. itemKey={itemKey}
  125. itemData={itemData}
  126. >
  127. {Row}
  128. </List>
  129. )}
  130. </AutoSizer>
  131. </Styled.Container>
  132. );
  133. };