diff --git a/packages/web/components/CoverRowVirtual.tsx b/packages/web/components/CoverRowVirtual.tsx
index b9cb543..f86219c 100644
--- a/packages/web/components/CoverRowVirtual.tsx
+++ b/packages/web/components/CoverRowVirtual.tsx
@@ -6,7 +6,7 @@ import { prefetchAlbum } from '@/web/api/hooks/useAlbum'
import { prefetchPlaylist } from '@/web/api/hooks/usePlaylist'
import { Virtuoso } from 'react-virtuoso'
import { useTranslation } from 'react-i18next'
-import {
+import React, {
CSSProperties,
FC,
ReactNode,
@@ -18,13 +18,13 @@ import {
} from 'react'
import { createPortal } from 'react-dom'
import humanNumber from 'human-number'
-import { useWhyDidYouUpdate } from 'ahooks'
const CoverRow = ({
albums,
playlists,
title,
className,
+ Footer,
}: {
title?: string
className?: string
@@ -32,6 +32,7 @@ const CoverRow = ({
playlists?: Playlist[]
containerClassName?: string
containerStyle?: CSSProperties
+ Footer?:React.FC
}) => {
const navigate = useNavigate()
const { showTrackListName } = useSettings()
@@ -219,19 +220,20 @@ const CoverRow = ({
style={{
height: 'calc(100vh - 132px)',
}}
+ components={{
+ Footer:Footer
+ }}
data={rows}
- overscan={15}
+ overscan={75}
itemSize={el => el.getBoundingClientRect().height + 24}
totalCount={rows.length}
- components={{
- Header: () =>
,
- // Footer: () =>
,
- }}
itemContent={(index, row) => (
- {row.map((item: Item) => (
+ {
+ row.map((item: Item) => (
- ))}
+ ))
+ }
)}
/>
diff --git a/packages/web/components/Tabs.tsx b/packages/web/components/Tabs.tsx
index 65e5bac..91aeb3b 100644
--- a/packages/web/components/Tabs.tsx
+++ b/packages/web/components/Tabs.tsx
@@ -28,12 +28,12 @@ function Tabs
({
onChange(tab.id)}
>
diff --git a/packages/web/components/TrackList/Track.tsx b/packages/web/components/TrackList/Track.tsx
new file mode 100644
index 0000000..2437446
--- /dev/null
+++ b/packages/web/components/TrackList/Track.tsx
@@ -0,0 +1,100 @@
+import Icon from '@/web/components/Icon'
+import Wave from '@/web/components/Animation/Wave'
+import { formatDuration, resizeImage } from '@/web/utils/common'
+import { State as PlayerState } from '@/web/utils/player'
+import { css, cx } from '@emotion/css'
+import { Fragment, useEffect } from 'react'
+import { NavLink } from 'react-router-dom'
+import React from 'react'
+
+const Track = ({
+ track,
+ index,
+ playingTrackID,
+ state,
+ handleClick,
+ }: {
+ track?: Track
+ index: number
+ playingTrackID: number
+ state: PlayerState
+ handleClick: (e: React.MouseEvent
, trackID: number) => void
+ }) => {
+ return (
+ track && handleClick(e, track.id)}
+ onContextMenu={e => track && handleClick(e, track.id)}
+ >
+ {/* Right part */}
+
+ {/* Cover */}
+
+
+ {/* Track Name and Artists */}
+
+
+ {track?.name}
+
+ {[1318912, 1310848].includes(track?.mark || 0) && (
+
+ )}
+
+
+ {track?.ar.map((a, index) => (
+
+ {index > 0 && ', '}
+
+ {a.name}
+
+
+ ))}
+
+
+
+ {/* Wave icon */}
+ {playingTrackID === track?.id && (
+
+
+
+ )}
+
+
+ {/* Album Name */}
+
+
+ {track?.al?.name}
+
+
+
+ {/* Duration */}
+
+ {formatDuration(track?.dt || 0, 'en-US', 'hh:mm:ss')}
+
+
+ )
+ }
+
+ export default Track
\ No newline at end of file
diff --git a/packages/web/components/TrackList.tsx b/packages/web/components/TrackList/TrackList.tsx
similarity index 99%
rename from packages/web/components/TrackList.tsx
rename to packages/web/components/TrackList/TrackList.tsx
index df9486e..7f736a6 100644
--- a/packages/web/components/TrackList.tsx
+++ b/packages/web/components/TrackList/TrackList.tsx
@@ -2,7 +2,7 @@ import { formatDuration } from '@/web/utils/common'
import { css, cx } from '@emotion/css'
import player from '@/web/states/player'
import { useSnapshot } from 'valtio'
-import Wave from './Animation/Wave'
+import Wave from '../Animation/Wave'
import Icon from '@/web/components/Icon'
import useIsMobile from '@/web/hooks/useIsMobile'
import useUserLikedTracksIDs, { useMutationLikeATrack } from '@/web/api/hooks/useUserLikedTracksIDs'
diff --git a/packages/web/pages/Playlist/TrackList.tsx b/packages/web/components/TrackList/TrackListVirtual.tsx
similarity index 92%
rename from packages/web/pages/Playlist/TrackList.tsx
rename to packages/web/components/TrackList/TrackListVirtual.tsx
index 2819cd8..f601090 100644
--- a/packages/web/pages/Playlist/TrackList.tsx
+++ b/packages/web/components/TrackList/TrackListVirtual.tsx
@@ -8,7 +8,6 @@ import { css, cx } from '@emotion/css'
import { Fragment, useEffect } from 'react'
import { NavLink } from 'react-router-dom'
import { useSnapshot } from 'valtio'
-import react from '@vitejs/plugin-react-swc'
import React from 'react'
import { Virtuoso } from 'react-virtuoso'
@@ -109,12 +108,14 @@ function TrackList({
onPlay,
className,
isLoading,
+ Header,
}: {
tracks?: Track[]
onPlay: (id: number) => void
className?: string
isLoading?: boolean
placeholderRows?: number
+ Header?: React.FC
}) {
const { trackID, state } = useSnapshot(player)
let playingTrack = tracks?.find(track => track.id === trackID)
@@ -136,20 +137,20 @@ function TrackList({
if (e.detail === 2) onPlay?.(trackID)
}
-
return (
-
+
el.getBoundingClientRect().height + 24}
totalCount={tracks?.length}
- itemContent={(index, row) => (
-
+ itemContent={(index) => (
-
)}
/>
)
}
-const TrackListMemo = React.memo(TrackList)
-TrackListMemo.displayName = "TrackList"
+const TrackListVirtualMemo = React.memo(TrackList)
+TrackListVirtualMemo.displayName = "TrackListVirtual"
-export default TrackListMemo
+export default TrackListVirtualMemo
diff --git a/packages/web/hooks/useIntersectionObserver.ts b/packages/web/hooks/useIntersectionObserver.ts
index c76f867..7bdbc2a 100644
--- a/packages/web/hooks/useIntersectionObserver.ts
+++ b/packages/web/hooks/useIntersectionObserver.ts
@@ -1,21 +1,29 @@
-import { useState, useEffect, RefObject } from 'react'
+import { useState, useEffect, RefObject } from 'react';
const useIntersectionObserver = (element: RefObject
): { onScreen: boolean } => {
- const [onScreen, setOnScreen] = useState(false)
+ const [onScreen, setOnScreen] = useState(false);
useEffect(() => {
- if (element.current) {
- const observer = new IntersectionObserver(([entry]) => setOnScreen(entry.isIntersecting))
- observer.observe(element.current)
- return () => {
- observer.disconnect()
- }
+ const supportsIntersectionObserver = 'IntersectionObserver' in window;
+
+ if (!supportsIntersectionObserver || !element?.current) {
+ console.warn('Intersection Observer is not supported in this browser or element is undefined.');
+ return;
}
- }, [element, setOnScreen])
+
+ const observer = new IntersectionObserver(([entry]) => {
+ setOnScreen(entry.isIntersecting)
+ },{threshold:0});
+ observer.observe(element.current);
+
+ return () => {
+ observer.disconnect();
+ };
+ }, [element, setOnScreen]);
return {
onScreen,
- }
-}
+ };
+};
-export default useIntersectionObserver
+export default useIntersectionObserver;
diff --git a/packages/web/package.json b/packages/web/package.json
index 5e3ce27..f3cde02 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -48,7 +48,7 @@
"react-router-dom": "^6.6.1",
"react-use": "^17.4.0",
"react-use-measure": "^2.1.1",
- "react-virtuoso": "^2.16.6",
+ "react-virtuoso": "^4.6.2",
"valtio": "^1.8.0",
"web-audio-api": "^0.2.2"
},
diff --git a/packages/web/pages/Album/Album.tsx b/packages/web/pages/Album/Album.tsx
index cb8ac39..5e86364 100644
--- a/packages/web/pages/Album/Album.tsx
+++ b/packages/web/pages/Album/Album.tsx
@@ -2,7 +2,7 @@ import useAlbum from '@/web/api/hooks/useAlbum'
import useTracks from '@/web/api/hooks/useTracks'
import { useParams } from 'react-router-dom'
import PageTransition from '@/web/components/PageTransition'
-import TrackList from '@/web/components/TrackList'
+import TrackList from '@/web/components/TrackList/TrackList'
import player from '@/web/states/player'
import toast from 'react-hot-toast'
import { useCallback } from 'react'
diff --git a/packages/web/pages/Artist/ArtistSongs.tsx b/packages/web/pages/Artist/ArtistSongs.tsx
index 8fd73cb..54973e0 100644
--- a/packages/web/pages/Artist/ArtistSongs.tsx
+++ b/packages/web/pages/Artist/ArtistSongs.tsx
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
import useArtistSongs from '@/web/api/hooks/useArtistSongs'
import useTracks from '@/web/api/hooks/useTracks'
import { FetchArtistSongsParams } from '@/shared/api/Artist'
-import TrackList from '../Playlist/TrackList'
+import TrackList from '../../components/TrackList/TrackListVirtual'
import player from '@/web/states/player'
import { useParams } from 'react-router-dom'
import ScrollPagination from '@/web/components/ScrollPage'
diff --git a/packages/web/pages/Browse/Hot.tsx b/packages/web/pages/Browse/Hot.tsx
index 6b053c7..9fd3d3c 100644
--- a/packages/web/pages/Browse/Hot.tsx
+++ b/packages/web/pages/Browse/Hot.tsx
@@ -1,40 +1,70 @@
import { fetchHQPlaylist } from '@/web/api/playlist'
+import Loading from '@/web/components/Animation/Loading'
import CoverRowVirtual from '@/web/components/CoverRowVirtual'
-import { memo, useEffect, useState } from 'react'
-import ScrollPagination from '@/web/components/ScrollPage'
+import useIntersectionObserver from '@/web/hooks/useIntersectionObserver'
+import { useWhyDidYouUpdate } from 'ahooks'
+import { throttle } from 'lodash-es'
+import { memo, useEffect, useRef, useState } from 'react'
const Hot = ({ cat }: { cat: string }) => {
const [dataSource, setDatasource] = useState([])
-
const [hasMore, setHasMore] = useState(true)
-
- const getData = async (pageNo: number, pageSize: number): Promise<{ hasMore: boolean }> => {
- if (hasMore === false) return { hasMore: false }
+ const [fetching, setFetching] = useState(false)
+ const [currentPage, setCurrentPage] = useState(1)
+ const getData = async (pageSize: number)=> {
+
+ if (hasMore === false) return
+ setFetching(true)
const resp = await fetchHQPlaylist({
cat: cat,
limit: pageSize || 50,
- before: (pageNo - 1) * pageSize || 0,
+ before: dataSource.length ? dataSource[dataSource.length-1].updateTime || 0 : 0,
})
+ setFetching(false)
setHasMore(resp.more)
-
+ if(!resp.more) return
+
+ if(dataSource === resp.playlists) return
let arrSource = [...dataSource, ...resp.playlists]
setDatasource([...new Set(arrSource)])
- return { hasMore: hasMore }
}
- useEffect(() => {
- setDatasource([])
+
+ const getDataThrottle = throttle((pageSize: number)=>{
+ getData(pageSize)
+ },1000)
+
+ useEffect(()=>{
setHasMore(true)
- getData(1, 50)
- }, [])
- const renderItems = () => {
- return
- }
+ getDataThrottle(50)
+ },[])
+
+ useEffect(()=>{
+ getDataThrottle(50)
+ },[currentPage])
+
+ const Footer = ()=>{
+ const observePoint = useRef(null)
+ const { onScreen: isScrollReachBottom } = useIntersectionObserver(observePoint)
+ const [prevState,setPrevState] = useState(false)
+ const loadMore = ()=>{
+ setCurrentPage(currentPage+1)
+ }
+ useEffect(()=>{
+ if(prevState != isScrollReachBottom && isScrollReachBottom && hasMore && !fetching){
+ setPrevState(isScrollReachBottom)
+ loadMore()
+ }
+ },[isScrollReachBottom])
+
+ return {hasMore && }
+ }
+
return (
<>
-
-
+
+
>
)
diff --git a/packages/web/pages/Browse/Top.tsx b/packages/web/pages/Browse/Top.tsx
index b502288..ff4563d 100644
--- a/packages/web/pages/Browse/Top.tsx
+++ b/packages/web/pages/Browse/Top.tsx
@@ -2,8 +2,10 @@ import { fetchTopPlaylist } from '@/web/api/playlist'
import { PlaylistApiNames } from '@/shared/api/Playlists'
import { useQuery } from '@tanstack/react-query'
import CoverRowVirtual from '@/web/components/CoverRowVirtual'
-import { memo, useCallback, useEffect, useState } from 'react'
+import { memo, useCallback, useEffect, useRef, useState } from 'react'
import ScrollPagination from '@/web/components/ScrollPage'
+import useIntersectionObserver from '@/web/hooks/useIntersectionObserver'
+import Loading from '@/web/components/Animation/Loading'
const reactQueryOptions = {
refetchOnWindowFocus: false,
@@ -13,20 +15,21 @@ const reactQueryOptions = {
const Top = ({ cat }: { cat: string }) => {
const [dataSource, setDatasource] = useState
([])
-
const [hasMore, setHasMore] = useState(true)
+ const [fetching, setFetching] = useState(false)
+ const [currentPage, setCurrentPage] = useState(1)
- const getData = async (pageNo: number, pageSize: number): Promise<{ hasMore: boolean }> => {
- console.log('top ', cat, ' ', pageNo, ' ', pageSize);
-
- if (hasMore === false) return { hasMore: false }
+ const getData = async (pageNo: number, pageSize: number) =>{
+ if (hasMore === false) return
+ setFetching(true)
const resp = await fetchTopPlaylist({
cat: cat,
limit: pageSize || 50,
offset: (pageNo - 1) * pageSize || 0,
})
-
+ setFetching(false)
setHasMore(resp.more)
+ if(!resp.more) return
let arrSource = [...dataSource, ...resp.playlists]
setDatasource([...new Set(arrSource)])
@@ -37,13 +40,32 @@ const Top = ({ cat }: { cat: string }) => {
setHasMore(true)
getData(1, 50)
}, [])
- const renderItems = () => {
- return
+
+ useEffect(()=>{
+ getData(currentPage,50)
+ },[currentPage])
+
+ const Footer = ()=>{
+ const observePoint = useRef(null)
+ const { onScreen: isScrollReachBottom } = useIntersectionObserver(observePoint)
+ const [prevState,setPrevState] = useState(false)
+ const loadMore = ()=>{
+ setCurrentPage(currentPage+1)
+ }
+ useEffect(()=>{
+ if(prevState != isScrollReachBottom && isScrollReachBottom && hasMore && !fetching){
+ setPrevState(isScrollReachBottom)
+ loadMore()
+ }
+ },[isScrollReachBottom])
+
+ return {hasMore && }
}
+
return (
<>
-
-
+
+
>
)
diff --git a/packages/web/pages/My/Cloud.tsx b/packages/web/pages/My/Cloud.tsx
index 2188e7a..582d029 100644
--- a/packages/web/pages/My/Cloud.tsx
+++ b/packages/web/pages/My/Cloud.tsx
@@ -1,8 +1,8 @@
-import { useEffect, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
import useArtistSongs from '@/web/api/hooks/useArtistSongs'
import useTracks from '@/web/api/hooks/useTracks'
import { FetchArtistSongsParams } from '@/shared/api/Artist'
-import TrackList from '../Playlist/TrackList'
+import TrackList from '../../components/TrackList/TrackListVirtual'
import player from '@/web/states/player'
import { useParams } from 'react-router-dom'
import ScrollPagination from '@/web/components/ScrollPage'
@@ -11,6 +11,16 @@ import { fetchTracks } from '@/web/api/track'
import toast from 'react-hot-toast'
import { CloudDiskInfoParam } from '@/shared/api/User'
import { cloudDisk } from '@/web/api/user'
+import { motion } from 'framer-motion'
+import Track from '@/web/components/TrackList/Track'
+import { openContextMenu } from '@/web/states/contextMenus'
+import { useSnapshot } from 'valtio'
+import useIntersectionObserver from '@/web/hooks/useIntersectionObserver'
+import { useThrottle } from 'react-use'
+import { has, throttle } from 'lodash-es'
+import Loading from '@/web/components/Animation/Loading'
+import { cx } from '@emotion/css'
+
const reactQueryOptions = {
refetchOnWindowFocus: false,
refetchInterval: 1000 * 60 * 60, // 1 hour
@@ -18,11 +28,13 @@ const reactQueryOptions = {
}
const Cloud = () => {
+ const {trackID, state} = useSnapshot(player)
const [dataSource, setDatasource] = useState