Rework music list
This commit is contained in:
@@ -184,8 +184,6 @@ function MusicItemComponent(props: MusicItemType) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Using `memo` to optimize rendering performance by memorizing the component's output.
|
|
||||||
// This ensures that the component only re-renders when its props change.
|
|
||||||
const MusicItem = memo(MusicItemComponent);
|
const MusicItem = memo(MusicItemComponent);
|
||||||
|
|
||||||
export default MusicItem;
|
export default MusicItem;
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import React, { useCallback, useState, useMemo, memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { FlatList, HStack, View, useBreakpointValue, useTheme, Text, Row } from 'native-base';
|
import { FlatList, HStack, View, useBreakpointValue, useTheme, Text, Row } from 'native-base';
|
||||||
import { ActivityIndicator, StyleSheet } from 'react-native';
|
import { ActivityIndicator, StyleSheet } from 'react-native';
|
||||||
import MusicItem, { MusicItemType } from './MusicItem';
|
import MusicItem from './MusicItem';
|
||||||
import ButtonBase from './ButtonBase';
|
import ButtonBase from './ButtonBase';
|
||||||
import { ArrowDown2, ArrowRotateLeft, Cup, Icon } from 'iconsax-react-native';
|
import { ArrowDown2, ArrowRotateLeft, Cup, Icon } from 'iconsax-react-native';
|
||||||
import { translate } from '../../i18n/i18n';
|
import { translate } from '../../i18n/i18n';
|
||||||
|
import Song from '../../models/Song';
|
||||||
|
import { useLikeSongMutation } from '../../utils/likeSongMutation';
|
||||||
|
import { useNavigation } from '../../Navigation';
|
||||||
|
import { LoadingView } from '../Loading';
|
||||||
|
|
||||||
// Props type definition for MusicItemTitle.
|
// Props type definition for MusicItemTitle.
|
||||||
interface MusicItemTitleProps {
|
interface MusicItemTitleProps {
|
||||||
@@ -47,167 +51,99 @@ function MusicItemTitleComponent(props: MusicItemTitleProps) {
|
|||||||
// MusicItemTitle component, memoized for performance.
|
// MusicItemTitle component, memoized for performance.
|
||||||
const MusicItemTitle = memo(MusicItemTitleComponent);
|
const MusicItemTitle = memo(MusicItemTitleComponent);
|
||||||
|
|
||||||
/**
|
const Header = () => {
|
||||||
* Define the type for the MusicList component props.
|
|
||||||
*/
|
|
||||||
type MusicListProps = {
|
|
||||||
/**
|
|
||||||
* Music items available for display. Not all items may be displayed initially;
|
|
||||||
* depends on 'musicsPerPage'.
|
|
||||||
*/
|
|
||||||
initialMusics: MusicItemType[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to load more music items asynchronously. Called with current page number
|
|
||||||
* and the list of all music items. Should return a Promise with additional music items.
|
|
||||||
*/
|
|
||||||
loadMoreMusics?: (page: number, musics: MusicItemType[]) => Promise<MusicItemType[]>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of music items to display per page. Determines initial and additional items displayed.
|
|
||||||
*/
|
|
||||||
musicsPerPage?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `MusicList` Component
|
|
||||||
*
|
|
||||||
* A responsive and dynamic list component designed for displaying a collection of music items.
|
|
||||||
* It allows for loading and rendering an initial set of music items and provides functionality
|
|
||||||
* to load more items dynamically as needed.
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Dynamically loads and displays music items based on the provided `initialMusics` and `musicsPerPage`.
|
|
||||||
* - Supports pagination through the `loadMoreMusics` function, which loads additional music items when invoked.
|
|
||||||
* - Adapts its layout responsively based on screen size for optimal viewing across different devices.
|
|
||||||
* - Includes a loading indicator to inform users when additional items are being loaded.
|
|
||||||
* - Conditionally renders a 'Load More' button to fetch more music items, hidden when no more items are available.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
*
|
|
||||||
* ```jsx
|
|
||||||
* <MusicList
|
|
||||||
* initialMusics={initialMusicData}
|
|
||||||
* loadMoreMusics={(page, currentMusics) => loadAdditionalMusics(page, currentMusics)}
|
|
||||||
* musicsPerPage={10}
|
|
||||||
* />
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Note:
|
|
||||||
* - The `MusicList` is designed to handle a potentially large number of music items efficiently,
|
|
||||||
* making it suitable for use cases where the list of items is expected to grow over time.
|
|
||||||
* - The layout and styling are optimized for performance and responsiveness.
|
|
||||||
*/
|
|
||||||
function MusicListComponent({
|
|
||||||
initialMusics,
|
|
||||||
loadMoreMusics,
|
|
||||||
musicsPerPage = loadMoreMusics ? 50 : initialMusics.length,
|
|
||||||
}: MusicListProps) {
|
|
||||||
// State initialization for MusicList.
|
|
||||||
// 'allMusics': all music items.
|
|
||||||
// 'displayedMusics': items displayed per page.
|
|
||||||
// 'currentPage': current page in pagination.
|
|
||||||
// 'loading': indicates if more items are being loaded.
|
|
||||||
// 'hasMoreMusics': flag for more items availability.
|
|
||||||
const [musicListState, setMusicListState] = useState({
|
|
||||||
allMusics: initialMusics,
|
|
||||||
displayedMusics: initialMusics.slice(0, musicsPerPage),
|
|
||||||
currentPage: 1,
|
|
||||||
loading: false,
|
|
||||||
hasMoreMusics: initialMusics.length > musicsPerPage || !!loadMoreMusics,
|
|
||||||
});
|
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const screenSize = useBreakpointValue({ base: 'small', md: 'md', xl: 'xl' });
|
const screenSize = useBreakpointValue({ base: 'small', md: 'md', xl: 'xl' });
|
||||||
const isBigScreen = screenSize === 'xl';
|
const isBigScreen = screenSize === 'xl';
|
||||||
|
|
||||||
// Loads additional music items.
|
return (
|
||||||
// Uses useCallback to avoid unnecessary redefinitions on re-renders.
|
<HStack
|
||||||
const loadMoreMusicItems = useCallback(async () => {
|
space={isBigScreen ? 1 : 2}
|
||||||
if (musicListState.loading || !musicListState.hasMoreMusics) {
|
style={{
|
||||||
return;
|
backgroundColor: colors.coolGray[500],
|
||||||
}
|
paddingHorizontal: isBigScreen ? 8 : 16,
|
||||||
|
paddingVertical: 12,
|
||||||
setMusicListState((prevState) => ({ ...prevState, loading: true }));
|
marginBottom: 2,
|
||||||
|
}}
|
||||||
let hasMoreMusics = true;
|
>
|
||||||
const nextEndIndex = (musicListState.currentPage + 1) * musicsPerPage;
|
<Text
|
||||||
let updatedAllMusics = musicListState.allMusics;
|
fontSize="lg"
|
||||||
|
|
||||||
if (loadMoreMusics && updatedAllMusics.length <= nextEndIndex) {
|
|
||||||
const newMusics = await loadMoreMusics(musicListState.currentPage, updatedAllMusics);
|
|
||||||
updatedAllMusics = [...updatedAllMusics, ...newMusics];
|
|
||||||
hasMoreMusics = newMusics.length > 0;
|
|
||||||
} else {
|
|
||||||
hasMoreMusics = updatedAllMusics.length > nextEndIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMusicListState((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
allMusics: updatedAllMusics,
|
|
||||||
displayedMusics: updatedAllMusics.slice(0, nextEndIndex),
|
|
||||||
currentPage: prevState.currentPage + 1,
|
|
||||||
loading: false,
|
|
||||||
hasMoreMusics: hasMoreMusics,
|
|
||||||
}));
|
|
||||||
}, [musicsPerPage, loadMoreMusics, musicListState]);
|
|
||||||
|
|
||||||
// useMemo to optimize performance by memorizing the header,
|
|
||||||
// preventing unnecessary re-renders.
|
|
||||||
const headerComponent = useMemo(
|
|
||||||
() => (
|
|
||||||
<HStack
|
|
||||||
space={isBigScreen ? 1 : 2}
|
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: colors.coolGray[500],
|
flex: 4,
|
||||||
paddingHorizontal: isBigScreen ? 8 : 16,
|
width: '100%',
|
||||||
paddingVertical: 12,
|
justifyContent: 'center',
|
||||||
marginBottom: 2,
|
paddingRight: 60,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
{translate('musicListTitleSong')}
|
||||||
fontSize="lg"
|
</Text>
|
||||||
style={{
|
<MusicItemTitle
|
||||||
flex: 4,
|
text={translate('musicListTitleLastScore')}
|
||||||
width: '100%',
|
icon={ArrowRotateLeft}
|
||||||
justifyContent: 'center',
|
isBigScreen={isBigScreen}
|
||||||
paddingRight: 60,
|
/>
|
||||||
}}
|
<MusicItemTitle
|
||||||
>
|
text={translate('musicListTitleBestScore')}
|
||||||
{translate('musicListTitleSong')}
|
icon={Cup}
|
||||||
</Text>
|
isBigScreen={isBigScreen}
|
||||||
{[
|
/>
|
||||||
{ text: translate('musicListTitleLastScore'), icon: ArrowRotateLeft },
|
</HStack>
|
||||||
{ text: translate('musicListTitleBestScore'), icon: Cup },
|
|
||||||
].map((value) => (
|
|
||||||
<MusicItemTitle
|
|
||||||
key={value.text + 'key'}
|
|
||||||
text={value.text}
|
|
||||||
icon={value.icon}
|
|
||||||
isBigScreen={isBigScreen}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</HStack>
|
|
||||||
),
|
|
||||||
[colors.coolGray[500], isBigScreen]
|
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function MusicListCC({
|
||||||
|
musics,
|
||||||
|
refetch,
|
||||||
|
hasMore,
|
||||||
|
isFetching,
|
||||||
|
fetchMore,
|
||||||
|
}: {
|
||||||
|
musics?: Song[];
|
||||||
|
refetch: () => Promise<unknown>;
|
||||||
|
hasMore?: boolean;
|
||||||
|
isFetching: boolean;
|
||||||
|
fetchMore?: () => Promise<unknown>;
|
||||||
|
}) {
|
||||||
|
const { mutateAsync } = useLikeSongMutation();
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
if (!musics) {
|
||||||
|
return <LoadingView />;
|
||||||
|
}
|
||||||
|
|
||||||
// FlatList: Renders list efficiently, only rendering visible items.
|
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
ListHeaderComponent={headerComponent}
|
ListHeaderComponent={Header}
|
||||||
data={musicListState.displayedMusics}
|
data={musics}
|
||||||
renderItem={({ item }) => <MusicItem style={{ marginBottom: 2 }} {...item} />}
|
renderItem={({ item: song }) => (
|
||||||
keyExtractor={(item) => item.artist + item.song}
|
<MusicItem
|
||||||
|
artist={song.artist!.name}
|
||||||
|
song={song.name}
|
||||||
|
image={song.cover}
|
||||||
|
lastScore={song.lastScore}
|
||||||
|
bestScore={song.bestScore}
|
||||||
|
liked={song.isLiked!}
|
||||||
|
onLike={(state: boolean) => {
|
||||||
|
mutateAsync({ songId: song.id, like: state }).then(() => refetch());
|
||||||
|
}}
|
||||||
|
onPlay={() => navigation.navigate('Play', { songId: song.id })}
|
||||||
|
style={{ marginBottom: 2 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
keyExtractor={(item) => item.id.toString()}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
musicListState.hasMoreMusics ? (
|
hasMore ? (
|
||||||
<View style={styles.footerContainer}>
|
<View style={styles.footerContainer}>
|
||||||
{musicListState.loading ? (
|
{isFetching ? (
|
||||||
<ActivityIndicator color={colors.primary[300]} />
|
<ActivityIndicator color={colors.primary[300]} />
|
||||||
) : (
|
) : (
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
style={{ borderRadius: 999 }}
|
style={{ borderRadius: 999 }}
|
||||||
onPress={loadMoreMusicItems}
|
onPress={() => {
|
||||||
|
fetchMore?.();
|
||||||
|
}}
|
||||||
icon={ArrowDown2}
|
icon={ArrowDown2}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -232,10 +168,4 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Using `memo` to optimize rendering performance by memorizing the component's output.
|
export default MusicListCC;
|
||||||
// This ensures that the component only re-renders when its props change.
|
|
||||||
const MusicList = memo(MusicListComponent, (prev, next) => {
|
|
||||||
return prev.initialMusics.length == next.initialMusics.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
export default MusicList;
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useBreakpointValue, useTheme } from 'native-base';
|
import { useBreakpointValue, useTheme } from 'native-base';
|
||||||
import { useWindowDimensions } from 'react-native';
|
import { useWindowDimensions } from 'react-native';
|
||||||
import {
|
import {
|
||||||
@@ -11,57 +10,20 @@ import {
|
|||||||
} from 'react-native-tab-view';
|
} from 'react-native-tab-view';
|
||||||
import { Heart, Clock, StatusUp, FolderCross } from 'iconsax-react-native';
|
import { Heart, Clock, StatusUp, FolderCross } from 'iconsax-react-native';
|
||||||
import { Scene } from 'react-native-tab-view/lib/typescript/src/types';
|
import { Scene } from 'react-native-tab-view/lib/typescript/src/types';
|
||||||
import { useNavigation } from '../Navigation';
|
|
||||||
import { Translate, TranslationKey } from '../i18n/i18n';
|
import { Translate, TranslationKey } from '../i18n/i18n';
|
||||||
import MusicList from '../components/UI/MusicList';
|
|
||||||
import { useQuery } from '../Queries';
|
import { useQuery } from '../Queries';
|
||||||
import API from '../API';
|
import API from '../API';
|
||||||
import { LoadingView } from '../components/Loading';
|
|
||||||
import { useLikeSongMutation } from '../utils/likeSongMutation';
|
|
||||||
import Song from '../models/Song';
|
import Song from '../models/Song';
|
||||||
|
import { useState } from 'react';
|
||||||
type MusicListCCProps = {
|
import MusicListCC from '../components/UI/MusicList';
|
||||||
data: Song[] | undefined;
|
|
||||||
isLoading: boolean;
|
|
||||||
refetch: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MusicListCC = ({ data, isLoading, refetch }: MusicListCCProps) => {
|
|
||||||
const navigation = useNavigation();
|
|
||||||
const { mutateAsync } = useLikeSongMutation();
|
|
||||||
const user = useQuery(API.getUserInfo);
|
|
||||||
|
|
||||||
const musics = (data ?? []).map((song) => {
|
|
||||||
const isLiked = song.likedByUsers?.some(({ userId }) => userId === user.data?.id) ?? false;
|
|
||||||
|
|
||||||
return {
|
|
||||||
artist: song.artist!.name,
|
|
||||||
song: song.name,
|
|
||||||
image: song.cover,
|
|
||||||
lastScore: song.lastScore,
|
|
||||||
bestScore: song.bestScore,
|
|
||||||
liked: isLiked,
|
|
||||||
onLike: (state: boolean) => {
|
|
||||||
mutateAsync({ songId: song.id, like: state }).then(() => refetch());
|
|
||||||
},
|
|
||||||
onPlay: () => navigation.navigate('Play', { songId: song.id }),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <LoadingView />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <MusicList initialMusics={musics} musicsPerPage={25} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FavoritesMusic = () => {
|
const FavoritesMusic = () => {
|
||||||
const likedSongs = useQuery(API.getLikedSongs(['artist', 'SongHistory', 'likedByUsers']));
|
const likedSongs = useQuery(API.getLikedSongs(['artist', 'SongHistory', 'likedByUsers']));
|
||||||
return (
|
return (
|
||||||
<MusicListCC
|
<MusicListCC
|
||||||
data={likedSongs.data?.map((x) => x.song)}
|
musics={likedSongs.data?.map((x) => x.song)}
|
||||||
isLoading={likedSongs.isLoading}
|
|
||||||
refetch={likedSongs.refetch}
|
refetch={likedSongs.refetch}
|
||||||
|
isFetching={likedSongs.isFetching}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -70,11 +32,9 @@ const RecentlyPlayedMusic = () => {
|
|||||||
const playHistory = useQuery(API.getUserPlayHistory(['artist', 'SongHistory', 'likedByUsers']));
|
const playHistory = useQuery(API.getUserPlayHistory(['artist', 'SongHistory', 'likedByUsers']));
|
||||||
return (
|
return (
|
||||||
<MusicListCC
|
<MusicListCC
|
||||||
data={
|
musics={playHistory.data?.map((x) => x.song) as Song[]}
|
||||||
playHistory.data?.filter((x) => x.song !== undefined).map((x) => x.song) as Song[]
|
|
||||||
}
|
|
||||||
isLoading={playHistory.isLoading}
|
|
||||||
refetch={playHistory.refetch}
|
refetch={playHistory.refetch}
|
||||||
|
isFetching={playHistory.isFetching}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -83,9 +43,9 @@ const StepUpMusic = () => {
|
|||||||
const nextStep = useQuery(API.getSongSuggestions(['artist', 'SongHistory', 'likedByUsers']));
|
const nextStep = useQuery(API.getSongSuggestions(['artist', 'SongHistory', 'likedByUsers']));
|
||||||
return (
|
return (
|
||||||
<MusicListCC
|
<MusicListCC
|
||||||
data={nextStep.data ?? []}
|
musics={nextStep.data}
|
||||||
isLoading={nextStep.isLoading}
|
|
||||||
refetch={nextStep.refetch}
|
refetch={nextStep.refetch}
|
||||||
|
isFetching={nextStep.isFetching}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -111,7 +71,7 @@ const getTabData = (key: string) => {
|
|||||||
|
|
||||||
const MusicTab = () => {
|
const MusicTab = () => {
|
||||||
const layout = useWindowDimensions();
|
const layout = useWindowDimensions();
|
||||||
const [index, setIndex] = React.useState(0);
|
const [index, setIndex] = useState(0);
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||||
const isSmallScreen = screenSize === 'small';
|
const isSmallScreen = screenSize === 'small';
|
||||||
|
|||||||
Reference in New Issue
Block a user