Procházet zdrojové kódy

feat: scroll queue

Fela Maslen před 5 roky
rodič
revize
2f665014de

+ 4 - 2
gmus-web/src/components/ui/cmus/views/queue.styles.ts

@@ -1,13 +1,15 @@
 import { rem } from 'polished';
 import styled from 'styled-components';
 
-import { ActiveHighlightRow, FlexColumn } from '../styled/layout';
+import { ActiveHighlightRow, FlexList } from '../styled/layout';
 
-export const Container = styled(FlexColumn)`
+export const Container = styled(FlexList)`
+  height: 100%;
   width: 100%;
 `;
 
 export const QueueSong = styled(ActiveHighlightRow)`
+  overflow: hidden;
   width: 100%;
 `;
 

+ 70 - 28
gmus-web/src/components/ui/cmus/views/queue.tsx

@@ -1,28 +1,50 @@
 import { AxiosInstance, AxiosResponse } from 'axios';
-import React, { useCallback, useContext, useEffect, useMemo } from 'react';
+import React, { CSSProperties, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
+import AutoSizer from 'react-virtualized-auto-sizer';
+import { FixedSizeList as List } from 'react-window';
 
 import { StateContext } from '../../../../context/state';
 import { useRequestCallback } from '../../../../hooks/request';
 import { Song } from '../../../../types';
+import { namedMemo } from '../../../../utils/component';
 import { getApiUrl } from '../../../../utils/url';
 import { queueInfoLoaded } from '../actions';
 import { CmusUIDispatchContext, CmusUIStateContext } from '../reducer';
+import { lineHeight, useAutoJumpyScroll } from '../utils/scroll';
 
 import * as Styled from './queue.styles';
+import { SongData } from './songs';
 
 export type Props = {
   currentSong: Song | null;
 };
 
+type QueueInfoData = Omit<SongData, 'parentActive' | 'queuePosition'>;
+
+const itemKey = (index: number, data: QueueInfoData[]): number => data[index].song.id;
+
+const Row = namedMemo<{ index: number; data: QueueInfoData[]; style: CSSProperties }>(
+  'QueueInfo',
+  ({ index, data, style }) => (
+    <Styled.QueueSong
+      active={data[index].active}
+      parentActive={true}
+      highlight={data[index].highlight}
+      style={style}
+    >
+      <Styled.Track>{data[index].song.track}</Styled.Track>
+      <Styled.Title>{data[index].song.title}</Styled.Title>
+      <Styled.Artist>{data[index].song.artist}</Styled.Artist>
+      <Styled.Album>{data[index].song.album}</Styled.Album>
+    </Styled.QueueSong>
+  ),
+);
+
 export const ViewQueue: React.FC<Props> = ({ currentSong }) => {
-  const {
-    player: { queue },
-  } = useContext(StateContext);
+  const globalState = useContext(StateContext);
 
   const dispatchUI = useContext(CmusUIDispatchContext);
-  const {
-    queue: { active },
-  } = useContext(CmusUIStateContext);
+  const state = useContext(CmusUIStateContext);
 
   const sendRequest = useCallback(
     (axios: AxiosInstance, query: number[]): Promise<AxiosResponse<Song[]>> =>
@@ -34,39 +56,59 @@ export const ViewQueue: React.FC<Props> = ({ currentSong }) => {
     sendRequest,
   });
 
-  useEffect(() => {
-    if (queue.length) {
-      fetchQueueInfo(queue);
-    }
-  }, [fetchQueueInfo, queue]);
-
   const orderedSongInfo = useMemo<Song[]>(
     () =>
-      queue
+      globalState.player.queue
         .map((id) => queueInfo?.find((compare) => compare.id === id))
         .filter((info: Song | undefined): info is Song => !!info),
-    [queueInfo, queue],
+    [globalState.player.queue, queueInfo],
+  );
+
+  const itemData = useMemo<QueueInfoData[]>(
+    () =>
+      orderedSongInfo.map<QueueInfoData>((info) => ({
+        song: info,
+        active: state.queue.active === info.id,
+        highlight: currentSong?.id === info.id,
+      })),
+    [orderedSongInfo, state.queue.active, currentSong?.id],
   );
 
+  useEffect(() => {
+    if (globalState.player.queue.length) {
+      fetchQueueInfo(globalState.player.queue);
+    }
+  }, [fetchQueueInfo, globalState.player.queue]);
+
   useEffect(() => {
     dispatchUI(queueInfoLoaded(orderedSongInfo));
   }, [dispatchUI, orderedSongInfo]);
 
+  const windowRef = useRef<HTMLDivElement>(null);
+  useAutoJumpyScroll(
+    windowRef,
+    state.queue.active === null
+      ? -1
+      : orderedSongInfo.findIndex(({ id }) => id === state.queue.active),
+  );
+
   return (
     <Styled.Container>
-      {orderedSongInfo.map((song) => (
-        <Styled.QueueSong
-          key={song.id}
-          active={active === song.id}
-          parentActive={true}
-          highlight={currentSong?.id === song.id}
-        >
-          <Styled.Track>{song.track}</Styled.Track>
-          <Styled.Title>{song.title}</Styled.Title>
-          <Styled.Artist>{song.artist}</Styled.Artist>
-          <Styled.Album>{song.album}</Styled.Album>
-        </Styled.QueueSong>
-      ))}
+      <AutoSizer>
+        {({ height, width }): React.ReactElement => (
+          <List
+            outerRef={windowRef}
+            height={height}
+            width={width}
+            itemCount={itemData.length}
+            itemSize={lineHeight}
+            itemKey={itemKey}
+            itemData={itemData}
+          >
+            {Row}
+          </List>
+        )}
+      </AutoSizer>
     </Styled.Container>
   );
 };

+ 1 - 1
gmus-web/src/components/ui/cmus/views/songs.tsx

@@ -18,7 +18,7 @@ type Props = {
   active: boolean;
 };
 
-type SongData = {
+export type SongData = {
   song: Song;
   active: boolean;
   parentActive: boolean;