2022-05-12 02:45:43 +08:00

316 lines
9.1 KiB
TypeScript

import { memo, useCallback, useEffect, useMemo } from 'react'
import Button, { Color as ButtonColor } from '@/web/components/Button'
import Skeleton from '@/web/components/Skeleton'
import SvgIcon from '@/web/components/SvgIcon'
import TracksList from '@/web/components/TracksList'
import usePlaylist from '@/web/hooks/usePlaylist'
import useScroll from '@/web/hooks/useScroll'
import useTracksInfinite from '@/web/hooks/useTracksInfinite'
import { player } from '@/web/store'
import { formatDate, resizeImage } from '@/web/utils/common'
import useUserPlaylists, {
useMutationLikeAPlaylist,
} from '@/web/hooks/useUserPlaylists'
import useUser from '@/web/hooks/useUser'
import {
Mode as PlayerMode,
TrackListSourceType,
State as PlayerState,
} from '@/web/utils/player'
import toast from 'react-hot-toast'
import { useParams } from 'react-router-dom'
import { useSnapshot } from 'valtio'
const PlayButton = ({
playlist,
handlePlay,
isLoading,
}: {
playlist: Playlist | undefined
handlePlay: () => void
isLoading: boolean
}) => {
const playerSnapshot = useSnapshot(player)
const isThisPlaylistPlaying = useMemo(
() =>
playerSnapshot.mode === PlayerMode.TrackList &&
playerSnapshot.trackListSource?.type === TrackListSourceType.Playlist &&
playerSnapshot.trackListSource?.id === playlist?.id,
[
playerSnapshot.mode,
playerSnapshot.trackListSource?.id,
playerSnapshot.trackListSource?.type,
playlist?.id,
]
)
const wrappedHandlePlay = () => {
if (isThisPlaylistPlaying) {
player.playOrPause()
} else {
handlePlay()
}
}
const isPlaying =
isThisPlaylistPlaying &&
[PlayerState.Playing, PlayerState.Loading].includes(playerSnapshot.state)
return (
<Button onClick={wrappedHandlePlay} isSkelton={isLoading}>
<SvgIcon
name={isPlaying ? 'pause' : 'play'}
className='-ml-1 mr-1 h-6 w-6'
/>
{isPlaying ? '暂停' : '播放'}
</Button>
)
}
const Header = memo(
({
playlist,
isLoading,
handlePlay,
}: {
playlist: Playlist | undefined
isLoading: boolean
handlePlay: () => void
}) => {
const coverUrl = resizeImage(playlist?.coverImgUrl || '', 'lg')
const mutationLikeAPlaylist = useMutationLikeAPlaylist()
const { data: userPlaylists } = useUserPlaylists()
const isThisPlaylistLiked = useMemo(() => {
if (!playlist) return false
return !!userPlaylists?.playlist?.find(p => p.id === playlist.id)
}, [playlist, userPlaylists?.playlist])
const { data: user } = useUser()
const isThisPlaylistCreatedByCurrentUser = useMemo(() => {
if (!playlist || !user) return false
return playlist.creator.userId === user?.profile?.userId
}, [playlist, user])
return (
<>
{/* Header background */}
<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]' />
<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>
</div>
<div className='grid grid-cols-[17rem_auto] items-center gap-9'>
{/* Cover */}
<div className='relative z-0 aspect-square self-start'>
{!isLoading && (
<div
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'
style={{
backgroundImage: `url("${coverUrl}")`,
}}
></div>
)}
{!isLoading && (
<img
src={coverUrl}
className='rounded-2xl border border-black border-opacity-5'
/>
)}
{isLoading && (
<Skeleton v-else className='h-full w-full rounded-2xl' />
)}
</div>
{/* <!-- Playlist info --> */}
<div className='z-10 flex h-full flex-col justify-between'>
{/* <!-- Playlist name --> */}
{!isLoading && (
<div className='text-4xl font-bold dark:text-white'>
{playlist?.name}
</div>
)}
{isLoading && (
<Skeleton v-else className='w-3/4 text-4xl'>
PLACEHOLDER
</Skeleton>
)}
{/* <!-- Playlist creator --> */}
{!isLoading && (
<div className='mt-5 text-lg font-medium text-gray-800 dark:text-gray-300'>
· <span>{playlist?.creator?.nickname}</span>
</div>
)}
{isLoading && (
<Skeleton v-else className='mt-5 w-64 text-lg'>
PLACEHOLDER
</Skeleton>
)}
{/* <!-- Playlist last update time & track count --> */}
{!isLoading && (
<div className='text-sm text-gray-500 dark:text-gray-400'>
{formatDate(playlist?.updateTime || 0, 'zh-CN')} ·{' '}
{playlist?.trackCount}
</div>
)}
{isLoading && (
<Skeleton v-else className='w-72 translate-y-px text-sm'>
PLACEHOLDER
</Skeleton>
)}
{/* <!-- Playlist description --> */}
{!isLoading && (
<div className='line-clamp-2 mt-5 min-h-[2.5rem] text-sm text-gray-500 dark:text-gray-400'>
{playlist?.description}
</div>
)}
{isLoading && (
<Skeleton v-else className='mt-5 min-h-[2.5rem] w-1/2 text-sm'>
PLACEHOLDER
</Skeleton>
)}
{/* <!-- Buttons --> */}
<div className='mt-5 flex gap-4'>
<PlayButton {...{ playlist, handlePlay, isLoading }} />
{!isThisPlaylistCreatedByCurrentUser && (
<Button
color={ButtonColor.Gray}
iconColor={
isThisPlaylistLiked ? ButtonColor.Primary : ButtonColor.Gray
}
isSkelton={isLoading}
onClick={() =>
playlist?.id && mutationLikeAPlaylist.mutate(playlist)
}
>
<SvgIcon
name={isThisPlaylistLiked ? 'heart' : 'heart-outline'}
className='h-6 w-6'
/>
</Button>
)}
<Button
color={ButtonColor.Gray}
iconColor={ButtonColor.Gray}
isSkelton={isLoading}
onClick={() => toast('施工中...')}
>
<SvgIcon name='more' className='h-6 w-6' />
</Button>
</div>
</div>
</div>
</>
)
}
)
Header.displayName = 'Header'
const Tracks = memo(
({
playlist,
handlePlay,
isLoadingPlaylist,
}: {
playlist: Playlist | undefined
handlePlay: (trackID: number | null) => void
isLoadingPlaylist: boolean
}) => {
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 (
<>
{isLoadingPlaylist ? (
<TracksList tracks={[]} isSkeleton={true} />
) : isLoadingTracks ? (
<TracksList
tracks={playlist?.tracks ?? []}
onTrackDoubleClick={handlePlay}
/>
) : (
<TracksList tracks={tracks} onTrackDoubleClick={handlePlay} />
)}
</>
)
}
)
Tracks.displayName = 'Tracks'
const Playlist = () => {
const params = useParams()
const { data: playlist, isLoading } = usePlaylist({
id: Number(params.id) || 0,
})
const handlePlay = useCallback(
(trackID: number | null = null) => {
if (!playlist?.playlist?.id) {
toast('无法播放歌单')
return
}
player.playPlaylist(playlist.playlist.id, trackID)
},
[playlist]
)
return (
<div className='mt-10'>
<Header
playlist={playlist?.playlist}
isLoading={isLoading}
handlePlay={handlePlay}
/>
<Tracks
playlist={playlist?.playlist}
handlePlay={handlePlay}
isLoadingPlaylist={isLoading}
/>
</div>
)
}
export default Playlist