2022-03-29 00:11:05 +08:00
|
|
|
import { memo } from 'react'
|
2022-03-13 14:40:38 +08:00
|
|
|
import Button, { Color as ButtonColor } from '@/components/Button'
|
|
|
|
import Skeleton from '@/components/Skeleton'
|
|
|
|
import SvgIcon from '@/components/SvgIcon'
|
|
|
|
import TracksList from '@/components/TracksList'
|
|
|
|
import usePlaylist from '@/hooks/usePlaylist'
|
|
|
|
import useScroll from '@/hooks/useScroll'
|
|
|
|
import useTracksInfinite from '@/hooks/useTracksInfinite'
|
|
|
|
import { player } from '@/store'
|
|
|
|
import { formatDate, resizeImage } from '@/utils/common'
|
|
|
|
|
|
|
|
const enableRenderLog = true
|
|
|
|
|
|
|
|
const Header = memo(
|
|
|
|
({
|
|
|
|
playlist,
|
|
|
|
isLoading,
|
|
|
|
handlePlay,
|
|
|
|
}: {
|
|
|
|
playlist: Playlist | undefined
|
|
|
|
isLoading: boolean
|
|
|
|
handlePlay: () => void
|
|
|
|
}) => {
|
|
|
|
if (enableRenderLog) console.debug('Rendering Playlist.tsx Header')
|
|
|
|
const coverUrl = resizeImage(playlist?.coverImgUrl || '', 'lg')
|
|
|
|
|
|
|
|
return (
|
2022-03-29 00:11:05 +08:00
|
|
|
<>
|
2022-03-13 14:40:38 +08:00
|
|
|
{/* Header background */}
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='absolute top-0 left-0 z-0 h-[24rem] w-full overflow-hidden'>
|
|
|
|
<img src={coverUrl} className='absolute top-0 w-full blur-[100px]' />
|
|
|
|
<img src={coverUrl} className='absolute top-0 w-full blur-[100px]' />
|
2022-03-30 16:54:11 +08:00
|
|
|
<div className='absolute top-0 h-full w-full bg-gradient-to-b from-white/[.85] to-white dark:from-black/50 dark:to-[#1d1d1d]'></div>
|
2022-03-13 14:40:38 +08:00
|
|
|
</div>
|
|
|
|
|
2022-03-21 02:03:25 +08:00
|
|
|
<div className='grid grid-cols-[17rem_auto] items-center gap-9'>
|
2022-03-13 14:40:38 +08:00
|
|
|
{/* Cover */}
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='relative z-0 aspect-square self-start'>
|
2022-03-13 14:40:38 +08:00
|
|
|
{!isLoading && (
|
|
|
|
<div
|
2022-03-17 19:30:43 +08:00
|
|
|
className='absolute top-3.5 z-[-1] h-full w-full scale-x-[.92] scale-y-[.96] rounded-2xl bg-cover opacity-40 blur-lg filter'
|
2022-03-13 14:40:38 +08:00
|
|
|
style={{
|
|
|
|
backgroundImage: `url("${coverUrl}")`,
|
|
|
|
}}
|
|
|
|
></div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{!isLoading && (
|
|
|
|
<img
|
|
|
|
src={coverUrl}
|
2022-03-17 19:30:43 +08:00
|
|
|
className='rounded-2xl border border-black border-opacity-5'
|
2022-03-13 14:40:38 +08:00
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{isLoading && (
|
2022-03-17 19:30:43 +08:00
|
|
|
<Skeleton v-else className='h-full w-full rounded-2xl' />
|
2022-03-13 14:40:38 +08:00
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* <!-- Playlist info --> */}
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='z-10 flex h-full flex-col justify-between'>
|
2022-03-13 14:40:38 +08:00
|
|
|
{/* <!-- Playlist name --> */}
|
|
|
|
{!isLoading && (
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='text-4xl font-bold dark:text-white'>
|
2022-03-13 14:40:38 +08:00
|
|
|
{playlist?.name}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{isLoading && (
|
2022-03-17 19:30:43 +08:00
|
|
|
<Skeleton v-else className='w-3/4 text-4xl'>
|
2022-03-13 14:40:38 +08:00
|
|
|
PLACEHOLDER
|
|
|
|
</Skeleton>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* <!-- Playlist creator --> */}
|
|
|
|
{!isLoading && (
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='mt-5 text-lg font-medium text-gray-800 dark:text-gray-300'>
|
2022-03-27 15:21:48 +08:00
|
|
|
歌单 · <span>{playlist?.creator?.nickname}</span>
|
2022-03-13 14:40:38 +08:00
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{isLoading && (
|
2022-03-17 19:30:43 +08:00
|
|
|
<Skeleton v-else className='mt-5 w-64 text-lg'>
|
2022-03-13 14:40:38 +08:00
|
|
|
PLACEHOLDER
|
|
|
|
</Skeleton>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* <!-- Playlist last update time & track count --> */}
|
|
|
|
{!isLoading && (
|
2022-03-21 02:03:25 +08:00
|
|
|
<div className='text-sm text-gray-500 dark:text-gray-400'>
|
|
|
|
更新于 {formatDate(playlist?.updateTime || 0, 'zh-CN')} ·{' '}
|
|
|
|
{playlist?.trackCount} 首歌
|
2022-03-13 14:40:38 +08:00
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{isLoading && (
|
2022-03-17 19:30:43 +08:00
|
|
|
<Skeleton v-else className='w-72 translate-y-px text-sm'>
|
2022-03-13 14:40:38 +08:00
|
|
|
PLACEHOLDER
|
|
|
|
</Skeleton>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* <!-- Playlist description --> */}
|
|
|
|
{!isLoading && (
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='line-clamp-2 mt-5 min-h-[2.5rem] text-sm text-gray-500 dark:text-gray-400'>
|
2022-03-13 14:40:38 +08:00
|
|
|
{playlist?.description}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{isLoading && (
|
2022-03-17 19:30:43 +08:00
|
|
|
<Skeleton v-else className='mt-5 min-h-[2.5rem] w-1/2 text-sm'>
|
2022-03-13 14:40:38 +08:00
|
|
|
PLACEHOLDER
|
|
|
|
</Skeleton>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* <!-- Buttons --> */}
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='mt-5 flex gap-4'>
|
2022-03-13 14:40:38 +08:00
|
|
|
<Button onClick={() => handlePlay()} isSkelton={isLoading}>
|
2022-03-21 02:03:25 +08:00
|
|
|
<SvgIcon name='play' className='-ml-1 mr-1 h-6 w-6' />
|
|
|
|
播放
|
2022-03-13 14:40:38 +08:00
|
|
|
</Button>
|
|
|
|
|
2022-03-17 14:45:04 +08:00
|
|
|
<Button
|
|
|
|
color={ButtonColor.Gray}
|
2022-03-21 02:03:25 +08:00
|
|
|
iconColor={ButtonColor.Gray}
|
2022-03-17 14:45:04 +08:00
|
|
|
isSkelton={isLoading}
|
2022-04-02 02:12:15 +08:00
|
|
|
onClick={() => toast('施工中...')}
|
2022-03-17 14:45:04 +08:00
|
|
|
>
|
2022-03-21 02:03:25 +08:00
|
|
|
<SvgIcon name='heart-outline' className='h-6 w-6' />
|
2022-03-13 14:40:38 +08:00
|
|
|
</Button>
|
|
|
|
|
|
|
|
<Button
|
|
|
|
color={ButtonColor.Gray}
|
|
|
|
iconColor={ButtonColor.Gray}
|
|
|
|
isSkelton={isLoading}
|
2022-04-02 02:12:15 +08:00
|
|
|
onClick={() => toast('施工中...')}
|
2022-03-13 14:40:38 +08:00
|
|
|
>
|
2022-03-21 02:03:25 +08:00
|
|
|
<SvgIcon name='more' className='h-6 w-6' />
|
2022-03-13 14:40:38 +08:00
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2022-03-29 00:11:05 +08:00
|
|
|
</>
|
2022-03-13 14:40:38 +08:00
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
Header.displayName = 'Header'
|
|
|
|
|
|
|
|
const Tracks = memo(
|
|
|
|
({
|
|
|
|
playlist,
|
|
|
|
handlePlay,
|
|
|
|
isLoadingPlaylist,
|
|
|
|
}: {
|
|
|
|
playlist: Playlist | undefined
|
|
|
|
handlePlay: (trackID: number | null) => void
|
|
|
|
isLoadingPlaylist: boolean
|
|
|
|
}) => {
|
|
|
|
if (enableRenderLog) console.debug('Rendering Playlist.tsx Tracks')
|
|
|
|
|
|
|
|
const {
|
|
|
|
data: tracksPages,
|
|
|
|
hasNextPage,
|
|
|
|
isLoading: isLoadingTracks,
|
|
|
|
isFetchingNextPage,
|
|
|
|
fetchNextPage,
|
|
|
|
} = useTracksInfinite({
|
|
|
|
ids: playlist?.trackIds?.map(t => t.id) || [],
|
|
|
|
})
|
|
|
|
|
|
|
|
const scroll = useScroll(document.getElementById('mainContainer'), {
|
|
|
|
throttle: 500,
|
|
|
|
offset: {
|
|
|
|
bottom: 256,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!scroll.arrivedState.bottom || !hasNextPage || isFetchingNextPage)
|
|
|
|
return
|
|
|
|
fetchNextPage()
|
|
|
|
}, [
|
|
|
|
fetchNextPage,
|
|
|
|
hasNextPage,
|
|
|
|
isFetchingNextPage,
|
|
|
|
scroll.arrivedState.bottom,
|
|
|
|
])
|
|
|
|
|
|
|
|
const tracks = useMemo(() => {
|
|
|
|
if (!tracksPages) return []
|
|
|
|
const allTracks: Track[] = []
|
|
|
|
tracksPages.pages.forEach(page => allTracks.push(...(page?.songs ?? [])))
|
|
|
|
return allTracks
|
|
|
|
}, [tracksPages])
|
|
|
|
|
|
|
|
return (
|
2022-03-29 00:11:05 +08:00
|
|
|
<>
|
2022-03-13 14:40:38 +08:00
|
|
|
{isLoadingPlaylist ? (
|
|
|
|
<TracksList tracks={[]} isSkeleton={true} />
|
|
|
|
) : isLoadingTracks ? (
|
|
|
|
<TracksList
|
|
|
|
tracks={playlist?.tracks ?? []}
|
|
|
|
onTrackDoubleClick={handlePlay}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<TracksList tracks={tracks} onTrackDoubleClick={handlePlay} />
|
|
|
|
)}
|
2022-03-29 00:11:05 +08:00
|
|
|
</>
|
2022-03-13 14:40:38 +08:00
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
Tracks.displayName = 'Tracks'
|
|
|
|
|
|
|
|
const Playlist = () => {
|
|
|
|
if (enableRenderLog) console.debug('Rendering Playlist.tsx Playlist')
|
|
|
|
|
|
|
|
const params = useParams()
|
|
|
|
const { data: playlist, isLoading } = usePlaylist({
|
|
|
|
id: Number(params.id) || 0,
|
|
|
|
})
|
|
|
|
|
|
|
|
const handlePlay = useCallback(
|
|
|
|
(trackID: number | null = null) => {
|
2022-04-02 18:46:08 +08:00
|
|
|
if (!playlist?.playlist?.id) {
|
|
|
|
toast('无法播放歌单')
|
2022-03-13 14:40:38 +08:00
|
|
|
return
|
|
|
|
}
|
2022-04-02 18:46:08 +08:00
|
|
|
player.playPlaylist(playlist.playlist.id, trackID)
|
2022-03-13 14:40:38 +08:00
|
|
|
},
|
|
|
|
[playlist]
|
|
|
|
)
|
|
|
|
|
|
|
|
return (
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='mt-10'>
|
2022-03-13 14:40:38 +08:00
|
|
|
<Header
|
|
|
|
playlist={playlist?.playlist}
|
|
|
|
isLoading={isLoading}
|
|
|
|
handlePlay={handlePlay}
|
|
|
|
/>
|
|
|
|
|
|
|
|
<Tracks
|
|
|
|
playlist={playlist?.playlist}
|
|
|
|
handlePlay={handlePlay}
|
|
|
|
isLoadingPlaylist={isLoading}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export default Playlist
|