2022-03-13 14:40:38 +08:00
|
|
|
import dayjs from 'dayjs'
|
|
|
|
import { Fragment } from 'react'
|
|
|
|
import { NavLink } from 'react-router-dom'
|
|
|
|
import Button, { Color as ButtonColor } from '@/components/Button'
|
|
|
|
import CoverRow, { Subtitle } from '@/components/CoverRow'
|
|
|
|
import Skeleton from '@/components/Skeleton'
|
|
|
|
import SvgIcon from '@/components/SvgIcon'
|
|
|
|
import TracksAlbum from '@/components/TracksAlbum'
|
|
|
|
import useAlbum from '@/hooks/useAlbum'
|
|
|
|
import useArtistAlbums from '@/hooks/useArtistAlbums'
|
|
|
|
import { player } from '@/store'
|
2022-03-17 14:45:04 +08:00
|
|
|
import { State as PlayerState } from '@/utils/player'
|
2022-03-17 19:30:43 +08:00
|
|
|
import {
|
|
|
|
formatDate,
|
|
|
|
formatDuration,
|
|
|
|
resizeImage,
|
|
|
|
scrollToTop,
|
|
|
|
} from '@/utils/common'
|
2022-03-13 14:40:38 +08:00
|
|
|
|
2022-03-17 14:45:04 +08:00
|
|
|
const PlayButton = ({
|
|
|
|
album,
|
|
|
|
handlePlay,
|
|
|
|
isLoading,
|
|
|
|
}: {
|
|
|
|
album: Album | undefined
|
|
|
|
isLoading: boolean
|
|
|
|
handlePlay: () => void
|
|
|
|
}) => {
|
|
|
|
const playerSnapshot = useSnapshot(player)
|
|
|
|
const isPlaying = useMemo(
|
|
|
|
() => playerSnapshot.state === PlayerState.PLAYING,
|
|
|
|
[playerSnapshot.state]
|
|
|
|
)
|
|
|
|
const isThisAlbumPlaying = useMemo(
|
|
|
|
() =>
|
|
|
|
playerSnapshot.trackListSource?.type === 'album' &&
|
|
|
|
playerSnapshot.trackListSource?.id === album?.id,
|
|
|
|
[playerSnapshot.trackListSource, album?.id]
|
|
|
|
)
|
|
|
|
|
|
|
|
const wrappedHandlePlay = () => {
|
|
|
|
if (isPlaying && isThisAlbumPlaying) {
|
|
|
|
player.pause()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (!isPlaying && isThisAlbumPlaying) {
|
|
|
|
player.play()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
handlePlay()
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Button onClick={wrappedHandlePlay} isSkelton={isLoading}>
|
|
|
|
<SvgIcon
|
|
|
|
name={isPlaying && isThisAlbumPlaying ? 'pause' : 'play'}
|
2022-03-21 02:03:25 +08:00
|
|
|
className='mr-1 -ml-1 h-6 w-6'
|
2022-03-17 14:45:04 +08:00
|
|
|
/>
|
|
|
|
{isPlaying && isThisAlbumPlaying ? '暂停' : '播放'}
|
|
|
|
</Button>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-03-13 14:40:38 +08:00
|
|
|
const Header = ({
|
|
|
|
album,
|
|
|
|
isLoading,
|
|
|
|
handlePlay,
|
|
|
|
}: {
|
|
|
|
album: Album | undefined
|
|
|
|
isLoading: boolean
|
|
|
|
handlePlay: () => void
|
|
|
|
}) => {
|
|
|
|
const coverUrl = resizeImage(album?.picUrl || '', 'lg')
|
|
|
|
|
|
|
|
const albumDuration = useMemo(() => {
|
|
|
|
const duration = album?.songs?.reduce((acc, cur) => acc + cur.dt, 0) || 0
|
2022-03-21 02:03:25 +08:00
|
|
|
return formatDuration(duration, 'zh-CN', 'hh[hr] mm[min]')
|
2022-03-13 14:40:38 +08:00
|
|
|
}, [album?.songs])
|
|
|
|
|
|
|
|
const [isCoverError, setCoverError] = useState(false)
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Fragment>
|
|
|
|
{/* 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'>
|
2022-03-13 14:40:38 +08:00
|
|
|
{coverUrl && !isCoverError && (
|
|
|
|
<Fragment>
|
|
|
|
<img
|
|
|
|
src={coverUrl}
|
2022-03-17 19:30:43 +08:00
|
|
|
className='absolute -top-full w-full blur-[100px]'
|
2022-03-13 14:40:38 +08:00
|
|
|
/>
|
|
|
|
<img
|
|
|
|
src={coverUrl}
|
2022-03-17 19:30:43 +08:00
|
|
|
className='absolute -top-full w-full blur-[100px]'
|
2022-03-13 14:40:38 +08:00
|
|
|
/>
|
|
|
|
</Fragment>
|
|
|
|
)}
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='absolute top-0 h-full w-full bg-gradient-to-b from-white/[.84] to-white dark:from-black/[.5] dark:to-[#1d1d1d]'></div>
|
2022-03-13 14:40:38 +08:00
|
|
|
</div>
|
|
|
|
|
2022-03-17 19:30:43 +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
|
|
|
{/* Neon shadow */}
|
|
|
|
{!isLoading && coverUrl && !isCoverError && (
|
|
|
|
<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 && isCoverError ? (
|
|
|
|
// Fallback cover
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='flex h-full w-full items-center justify-center rounded-2xl border border-black border-opacity-5 bg-gray-100 text-gray-300'>
|
|
|
|
<SvgIcon name='music-note' className='h-1/2 w-1/2' />
|
2022-03-13 14:40:38 +08:00
|
|
|
</div>
|
|
|
|
) : (
|
|
|
|
coverUrl && (
|
|
|
|
<img
|
|
|
|
src={coverUrl}
|
2022-03-17 19:30:43 +08:00
|
|
|
className='rounded-2xl border border-b-0 border-black border-opacity-5 dark:border-white dark:border-opacity-5'
|
2022-03-13 14:40:38 +08:00
|
|
|
onError={() => setCoverError(true)}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
)}
|
2022-03-17 19:30:43 +08:00
|
|
|
{isLoading && <Skeleton className='h-full w-full rounded-2xl' />}
|
2022-03-13 14:40:38 +08:00
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* 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
|
|
|
{/* Name */}
|
2022-03-17 14:45:04 +08:00
|
|
|
{isLoading ? (
|
2022-03-17 19:30:43 +08:00
|
|
|
<Skeleton className='w-3/4 text-6xl'>PLACEHOLDER</Skeleton>
|
2022-03-17 14:45:04 +08:00
|
|
|
) : (
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='text-6xl font-bold dark:text-white'>
|
2022-03-13 14:40:38 +08:00
|
|
|
{album?.name}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* Artist */}
|
2022-03-17 14:45:04 +08:00
|
|
|
{isLoading ? (
|
2022-03-17 19:30:43 +08:00
|
|
|
<Skeleton className='mt-5 w-64 text-lg'>PLACEHOLDER</Skeleton>
|
2022-03-17 14:45:04 +08:00
|
|
|
) : (
|
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-13 14:40:38 +08:00
|
|
|
Album by{' '}
|
|
|
|
<NavLink
|
2022-03-23 01:21:22 +08:00
|
|
|
to={`/artist/${album?.artist.id}`}
|
2022-03-17 19:30:43 +08:00
|
|
|
className='cursor-default font-semibold hover:underline'
|
2022-03-13 14:40:38 +08:00
|
|
|
>
|
|
|
|
{album?.artist.name}
|
|
|
|
</NavLink>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* Release date & track count & album duration */}
|
2022-03-17 14:45:04 +08:00
|
|
|
{isLoading ? (
|
2022-03-17 19:30:43 +08:00
|
|
|
<Skeleton className='w-72 translate-y-px text-sm'>
|
2022-03-17 14:45:04 +08:00
|
|
|
PLACEHOLDER
|
|
|
|
</Skeleton>
|
|
|
|
) : (
|
2022-03-21 02:03:25 +08:00
|
|
|
<div className='text-sm text-gray-500 dark:text-gray-400'>
|
|
|
|
{dayjs(album?.publishTime || 0).year()} · {album?.size} 首歌,{' '}
|
2022-03-13 14:40:38 +08:00
|
|
|
{albumDuration}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* Description */}
|
2022-03-17 14:45:04 +08:00
|
|
|
{isLoading ? (
|
2022-03-17 19:30:43 +08:00
|
|
|
<Skeleton className='mt-5 min-h-[2.5rem] w-1/2 text-sm'>
|
2022-03-13 14:40:38 +08:00
|
|
|
PLACEHOLDER
|
|
|
|
</Skeleton>
|
2022-03-17 14:45:04 +08:00
|
|
|
) : (
|
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-17 14:45:04 +08:00
|
|
|
{album?.description}
|
|
|
|
</div>
|
2022-03-13 14:40:38 +08:00
|
|
|
)}
|
|
|
|
|
|
|
|
{/* Buttons */}
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='mt-5 flex gap-4'>
|
2022-03-17 14:45:04 +08:00
|
|
|
<PlayButton {...{ album, handlePlay, isLoading }} />
|
2022-03-13 14:40:38 +08:00
|
|
|
|
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}
|
|
|
|
onClick={() => toast('Work in progress')}
|
|
|
|
>
|
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-03-17 14:45:04 +08:00
|
|
|
onClick={() => toast('Work in progress')}
|
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>
|
|
|
|
</Fragment>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const MoreAlbum = ({ album }: { album: Album | undefined }) => {
|
|
|
|
// Fetch artist's albums
|
|
|
|
const { data: albums, isLoading } = useArtistAlbums({
|
|
|
|
id: album?.artist.id ?? 0,
|
|
|
|
limit: 1000,
|
|
|
|
})
|
|
|
|
|
|
|
|
const filteredAlbums = useMemo((): Album[] => {
|
|
|
|
if (!albums) return []
|
|
|
|
const allReleases = albums?.hotAlbums || []
|
|
|
|
const filteredAlbums = allReleases.filter(
|
|
|
|
album =>
|
|
|
|
['专辑', 'EP/Single', 'EP'].includes(album.type) && album.size > 1
|
|
|
|
)
|
|
|
|
const singles = allReleases.filter(album => album.type === 'Single')
|
|
|
|
|
|
|
|
const qualifiedAlbums = [...filteredAlbums, ...singles]
|
|
|
|
|
2022-03-17 19:30:43 +08:00
|
|
|
const formatName = (name: string) =>
|
|
|
|
name.toLowerCase().replace(/(\s|deluxe|edition|\(|\))/g, '')
|
|
|
|
|
2022-03-13 14:40:38 +08:00
|
|
|
const uniqueAlbums: Album[] = []
|
|
|
|
qualifiedAlbums.forEach(a => {
|
|
|
|
// 去除当前页面的专辑
|
2022-03-17 19:30:43 +08:00
|
|
|
if (formatName(a.name) === formatName(album?.name ?? '')) return
|
2022-03-13 14:40:38 +08:00
|
|
|
|
|
|
|
// 去除重复的专辑(包含 deluxe edition 的专辑会视为重复)
|
|
|
|
if (
|
|
|
|
uniqueAlbums.findIndex(aa => {
|
2022-03-17 19:30:43 +08:00
|
|
|
return formatName(a.name) === formatName(aa.name)
|
2022-03-13 14:40:38 +08:00
|
|
|
}) !== -1
|
|
|
|
) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// 去除 remix 专辑
|
|
|
|
if (
|
|
|
|
a.name.toLowerCase().includes('remix)') ||
|
|
|
|
a.name.toLowerCase().includes('remixes)')
|
|
|
|
) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
uniqueAlbums.push(a)
|
|
|
|
})
|
|
|
|
|
|
|
|
return uniqueAlbums.slice(0, 5)
|
2022-03-17 19:30:43 +08:00
|
|
|
}, [album?.name, albums])
|
2022-03-13 14:40:38 +08:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='my-5 h-px w-full bg-gray-100 dark:bg-gray-800'></div>
|
|
|
|
<div className='pl-px text-[1.375rem] font-semibold text-gray-800 dark:text-gray-100'>
|
2022-03-13 14:40:38 +08:00
|
|
|
More by{' '}
|
|
|
|
<NavLink
|
|
|
|
to={`/artist/${album?.artist?.id}`}
|
2022-03-17 19:30:43 +08:00
|
|
|
className='cursor-default hover:underline'
|
2022-03-13 14:40:38 +08:00
|
|
|
>
|
|
|
|
{album?.artist.name}
|
|
|
|
</NavLink>
|
|
|
|
</div>
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='mt-3'>
|
2022-03-13 14:40:38 +08:00
|
|
|
<CoverRow
|
|
|
|
albums={filteredAlbums}
|
|
|
|
subtitle={Subtitle.TYPE_RELEASE_YEAR}
|
|
|
|
isSkeleton={isLoading}
|
2022-03-17 19:30:43 +08:00
|
|
|
rows={1}
|
|
|
|
navigateCallback={scrollToTop}
|
2022-03-13 14:40:38 +08:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const Album = () => {
|
|
|
|
const params = useParams()
|
|
|
|
const { data: album, isLoading } = useAlbum({
|
|
|
|
id: Number(params.id) || 0,
|
|
|
|
})
|
|
|
|
|
|
|
|
const handlePlay = async (trackID: number | null = null) => {
|
|
|
|
const realAlbum = album?.album
|
|
|
|
if (!realAlbum) {
|
|
|
|
toast('Failed to play album')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
await player.playAlbum(realAlbum, trackID)
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='mt-10'>
|
2022-03-13 14:40:38 +08:00
|
|
|
<Header
|
|
|
|
album={album?.album}
|
|
|
|
isLoading={isLoading}
|
|
|
|
handlePlay={handlePlay}
|
|
|
|
/>
|
|
|
|
<TracksAlbum
|
|
|
|
tracks={album?.album.songs ?? []}
|
|
|
|
onTrackDoubleClick={handlePlay}
|
|
|
|
isSkeleton={isLoading}
|
|
|
|
/>
|
|
|
|
{album?.album && (
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='mt-5 text-xs text-gray-400'>
|
2022-03-13 14:40:38 +08:00
|
|
|
<div> Released {formatDate(album.album.publishTime || 0, 'en')} </div>
|
|
|
|
{album.album.company && (
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='mt-[2px]'>© {album.album.company} </div>
|
2022-03-13 14:40:38 +08:00
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{!isLoading && <MoreAlbum album={album?.album} />}
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export default Album
|