mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-17 21:58:03 +00:00
feat: updates
This commit is contained in:
parent
47e41dea9b
commit
ebebf2a733
160 changed files with 4148 additions and 2001 deletions
54
packages/web/pages/New/Album/Album.tsx
Normal file
54
packages/web/pages/New/Album/Album.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import TrackListHeader from '@/web/components/New/TrackListHeader'
|
||||
import useAlbum from '@/web/api/hooks/useAlbum'
|
||||
import useTracks from '@/web/api/hooks/useTracks'
|
||||
import { NavLink, useParams } from 'react-router-dom'
|
||||
import PageTransition from '@/web/components/New/PageTransition'
|
||||
import TrackList from '@/web/components/New/TrackList'
|
||||
import player from '@/web/states/player'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import useArtistAlbums from '@/web/api/hooks/useArtistAlbums'
|
||||
import { css, cx } from '@emotion/css'
|
||||
import CoverRow from '@/web/components/New/CoverRow'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import MoreByArtist from './MoreByArtist'
|
||||
import Header from './Header'
|
||||
|
||||
const Album = () => {
|
||||
const params = useParams()
|
||||
const { data: album } = useAlbum({
|
||||
id: Number(params.id),
|
||||
})
|
||||
|
||||
const { data: tracks } = useTracks({
|
||||
ids: album?.songs?.map(track => track.id) ?? [],
|
||||
})
|
||||
|
||||
const onPlay = useCallback(
|
||||
async (trackID: number | null = null) => {
|
||||
if (!album?.album?.id) {
|
||||
toast('无法播放专辑,该专辑不存在')
|
||||
return
|
||||
}
|
||||
player.playAlbum(album.album.id, trackID)
|
||||
},
|
||||
[album?.album?.id]
|
||||
)
|
||||
|
||||
return (
|
||||
<PageTransition>
|
||||
<Header />
|
||||
<TrackList
|
||||
tracks={tracks?.songs || album?.album.songs || album?.songs}
|
||||
className='z-10 mx-2.5 mt-3 lg:mx-0 lg:mt-10'
|
||||
onPlay={onPlay}
|
||||
/>
|
||||
<MoreByArtist album={album?.album} />
|
||||
|
||||
{/* Page padding */}
|
||||
<div className='h-16'></div>
|
||||
</PageTransition>
|
||||
)
|
||||
}
|
||||
|
||||
export default Album
|
||||
107
packages/web/pages/New/Album/Header.tsx
Normal file
107
packages/web/pages/New/Album/Header.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import useAlbum from '@/web/api/hooks/useAlbum'
|
||||
import useUserAlbums, {
|
||||
useMutationLikeAAlbum,
|
||||
} from '@/web/api/hooks/useUserAlbums'
|
||||
import Icon from '@/web/components/Icon'
|
||||
import TrackListHeader from '@/web/components/New/TrackListHeader'
|
||||
import useAppleMusicAlbum from '@/web/hooks/useAppleMusicAlbum'
|
||||
import useVideoCover from '@/web/hooks/useVideoCover'
|
||||
import player from '@/web/states/player'
|
||||
import { formatDuration } from '@/web/utils/common'
|
||||
import dayjs from 'dayjs'
|
||||
import { useMemo } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
const Header = () => {
|
||||
const params = useParams()
|
||||
const { data: userLikedAlbums } = useUserAlbums()
|
||||
|
||||
const { data: albumRaw } = useAlbum({
|
||||
id: Number(params.id),
|
||||
})
|
||||
const album = useMemo(() => albumRaw?.album, [albumRaw])
|
||||
|
||||
const { data: albumFromApple, isLoading: isLoadingAlbumFromApple } =
|
||||
useAppleMusicAlbum({
|
||||
id: album?.id,
|
||||
name: album?.name,
|
||||
artist: album?.artist.name,
|
||||
})
|
||||
|
||||
const { data: videoCoverFromRemote } = useVideoCover({
|
||||
id: album?.id,
|
||||
name: album?.name,
|
||||
artist: album?.artist.name,
|
||||
enabled: !window.env?.isElectron,
|
||||
})
|
||||
|
||||
// For <Cover />
|
||||
const cover = album?.picUrl
|
||||
const videoCover =
|
||||
albumFromApple?.attributes?.editorialVideo?.motionSquareVideo1x1?.video ||
|
||||
videoCoverFromRemote?.video
|
||||
|
||||
// For <Info />
|
||||
const title = album?.name
|
||||
const creatorName = album?.artist.name
|
||||
const creatorLink = `/artist/${album?.artist.id}`
|
||||
const description = isLoadingAlbumFromApple
|
||||
? ''
|
||||
: albumFromApple?.attributes?.editorialNotes?.standard || album?.description
|
||||
const extraInfo = useMemo(() => {
|
||||
const duration = album?.songs?.reduce((acc, cur) => acc + cur.dt, 0) || 0
|
||||
const albumDuration = formatDuration(duration, 'en', 'hh[hr] mm[min]')
|
||||
return (
|
||||
<>
|
||||
{album?.mark === 1056768 && (
|
||||
<Icon
|
||||
name='explicit'
|
||||
className='mb-px mr-1 h-3 w-3 lg:h-3.5 lg:w-3.5'
|
||||
/>
|
||||
)}{' '}
|
||||
{dayjs(album?.publishTime || 0).year()} · {album?.songs.length} tracks,{' '}
|
||||
{albumDuration}
|
||||
</>
|
||||
)
|
||||
}, [album])
|
||||
|
||||
// For <Actions />
|
||||
const isLiked = useMemo(() => {
|
||||
const id = Number(params.id)
|
||||
if (!id) return false
|
||||
return !!userLikedAlbums?.data.find(item => item.id === id)
|
||||
}, [params.id, userLikedAlbums?.data])
|
||||
|
||||
const onPlay = async (trackID: number | null = null) => {
|
||||
if (!album?.id) {
|
||||
toast('无法播放专辑,该专辑不存在')
|
||||
return
|
||||
}
|
||||
player.playAlbum(album.id, trackID)
|
||||
}
|
||||
|
||||
const likeAAlbum = useMutationLikeAAlbum()
|
||||
const onLike = async () => {
|
||||
likeAAlbum.mutateAsync(album?.id || Number(params.id))
|
||||
}
|
||||
|
||||
return (
|
||||
<TrackListHeader
|
||||
{...{
|
||||
title,
|
||||
creatorName,
|
||||
creatorLink,
|
||||
description,
|
||||
extraInfo,
|
||||
cover,
|
||||
videoCover,
|
||||
isLiked,
|
||||
onLike,
|
||||
onPlay,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
|
|
@ -11,7 +11,6 @@ import useArtistAlbums from '@/web/api/hooks/useArtistAlbums'
|
|||
import { css, cx } from '@emotion/css'
|
||||
import CoverRow from '@/web/components/New/CoverRow'
|
||||
import { useMemo } from 'react'
|
||||
import 'plyr-react/plyr.css'
|
||||
|
||||
const MoreByArtist = ({ album }: { album?: Album }) => {
|
||||
const { data: albums } = useArtistAlbums({
|
||||
|
|
@ -84,48 +83,4 @@ const MoreByArtist = ({ album }: { album?: Album }) => {
|
|||
)
|
||||
}
|
||||
|
||||
const Album = () => {
|
||||
const params = useParams()
|
||||
const { data: album } = useAlbum({
|
||||
id: Number(params.id),
|
||||
})
|
||||
|
||||
const { data: tracks } = useTracks({
|
||||
ids: album?.songs?.map(track => track.id) ?? [],
|
||||
})
|
||||
|
||||
const playerSnapshot = useSnapshot(player)
|
||||
|
||||
const onPlay = async (trackID: number | null = null) => {
|
||||
if (!album?.album.id) {
|
||||
toast('无法播放专辑,该专辑不存在')
|
||||
return
|
||||
}
|
||||
if (
|
||||
playerSnapshot.trackListSource?.type === 'album' &&
|
||||
playerSnapshot.trackListSource?.id === album.album.id
|
||||
) {
|
||||
await player.playTrack(trackID ?? album.songs[0].id)
|
||||
return
|
||||
}
|
||||
await player.playAlbum(album.album.id, trackID)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageTransition>
|
||||
<TrackListHeader
|
||||
album={album?.album}
|
||||
onPlay={onPlay}
|
||||
className='mt-2.5 lg:mt-0'
|
||||
/>
|
||||
<TrackList
|
||||
tracks={tracks?.songs || album?.album.songs || album?.songs}
|
||||
className='z-10 mx-2.5 mt-3 lg:mx-0 lg:mt-10'
|
||||
onPlay={onPlay}
|
||||
/>
|
||||
<MoreByArtist album={album?.album} />
|
||||
</PageTransition>
|
||||
)
|
||||
}
|
||||
|
||||
export default Album
|
||||
export default MoreByArtist
|
||||
2
packages/web/pages/New/Album/index.tsx
Normal file
2
packages/web/pages/New/Album/index.tsx
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import Album from './Album'
|
||||
export default Album
|
||||
|
|
@ -5,6 +5,7 @@ import Header from './Header'
|
|||
import Popular from './Popular'
|
||||
import ArtistAlbum from './ArtistAlbums'
|
||||
import FansAlsoLike from './FansAlsoLike'
|
||||
import ArtistMVs from './ArtistMVs'
|
||||
|
||||
const Artist = () => {
|
||||
const params = useParams()
|
||||
|
|
@ -17,13 +18,16 @@ const Artist = () => {
|
|||
<div>
|
||||
<Header artist={artist?.artist} />
|
||||
|
||||
{/* Dividing line */}
|
||||
<div className='mt-10 mb-7.5 h-px w-full bg-white/20'></div>
|
||||
|
||||
<Popular tracks={artist?.hotSongs} />
|
||||
|
||||
<ArtistAlbum />
|
||||
|
||||
<ArtistMVs />
|
||||
<FansAlsoLike />
|
||||
|
||||
{/* Page padding */}
|
||||
<div className='h-16'></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import useArtistAlbums from '@/web/api/hooks/useArtistAlbums'
|
||||
import CoverRow from '@/web/components/New/CoverRow'
|
||||
import React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
|
|
@ -11,7 +12,18 @@ const ArtistAlbum = () => {
|
|||
limit: 1000,
|
||||
})
|
||||
|
||||
const albums = useMemo(() => albumsRaw?.hotAlbums, [albumsRaw?.hotAlbums])
|
||||
const pages = useMemo(() => {
|
||||
const pages: Album[][] = []
|
||||
albumsRaw?.hotAlbums.forEach((album, index) => {
|
||||
const pageNo = Math.floor(index / 12)
|
||||
if (!pages[pageNo]) {
|
||||
pages[pageNo] = [album]
|
||||
} else {
|
||||
pages[pageNo].push(album)
|
||||
}
|
||||
})
|
||||
return pages
|
||||
}, [albumsRaw?.hotAlbums])
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -19,9 +31,19 @@ const ArtistAlbum = () => {
|
|||
Albums
|
||||
</div>
|
||||
|
||||
<CoverRow albums={albums?.slice(0, 12)} />
|
||||
<div className='no-scrollbar flex gap-6 overflow-y-hidden overflow-x-scroll'>
|
||||
{pages.map((page, index) => (
|
||||
<CoverRow
|
||||
key={index}
|
||||
albums={page}
|
||||
className='h-full w-full flex-shrink-0'
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ArtistAlbum
|
||||
const memoized = React.memo(ArtistAlbum)
|
||||
memoized.displayName = 'ArtistAlbum'
|
||||
export default memoized
|
||||
|
|
|
|||
32
packages/web/pages/New/Artist/ArtistMVs.tsx
Normal file
32
packages/web/pages/New/Artist/ArtistMVs.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import useArtistMV from '@/web/api/hooks/useArtistMV'
|
||||
|
||||
const ArtistMVs = () => {
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { data: videos } = useArtistMV({ id: Number(params.id) || 0 })
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mb-6 mt-10 text-12 font-medium uppercase text-neutral-300'>
|
||||
MV
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-3 gap-6'>
|
||||
{videos?.mvs?.slice(0, 6)?.map(video => (
|
||||
<div key={video.id} onClick={() => navigate(`/mv/${video.id}`)}>
|
||||
<img
|
||||
src={video.imgurl16v9}
|
||||
className='aspect-video w-full rounded-24 object-contain'
|
||||
/>
|
||||
<div className='mt-2 text-12 font-medium text-neutral-600'>
|
||||
{video.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ArtistMVs
|
||||
|
|
@ -4,16 +4,22 @@ import { useParams } from 'react-router-dom'
|
|||
|
||||
const FansAlsoLike = () => {
|
||||
const params = useParams()
|
||||
const { data: artists } = useSimilarArtists({ id: Number(params.id) || 0 })
|
||||
const { data: artists, isLoading } = useSimilarArtists({
|
||||
id: Number(params.id) || 0,
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mb-6 mt-10 text-12 font-medium uppercase text-neutral-300'>
|
||||
Fans Also Like
|
||||
</div>
|
||||
<>
|
||||
{(isLoading || artists?.artists) && (
|
||||
<div>
|
||||
<div className='mb-6 mt-10 text-12 font-medium uppercase text-neutral-300'>
|
||||
Fans Also Like
|
||||
</div>
|
||||
|
||||
<ArtistRow artists={artists?.artists?.slice(0, 5)} />
|
||||
</div>
|
||||
<ArtistRow artists={artists?.artists?.slice(0, 5)} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,44 @@
|
|||
import useUserArtists, {
|
||||
useMutationLikeAArtist,
|
||||
} from '@/web/api/hooks/useUserArtists'
|
||||
import Icon from '@/web/components/Icon'
|
||||
import player from '@/web/states/player'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
const Actions = () => {
|
||||
const { data: likedArtists } = useUserArtists()
|
||||
const params = useParams()
|
||||
const id = Number(params.id) || 0
|
||||
const isLiked = !!likedArtists?.data?.find(artist => artist.id === id)
|
||||
const likeArtist = useMutationLikeAArtist()
|
||||
|
||||
return (
|
||||
<div className='mt-11 flex items-end justify-between lg:z-10 lg:mt-6'>
|
||||
<div className='flex items-end'>
|
||||
<button className='mr-2.5 h-14 w-14 rounded-full dark:bg-white/10'></button>
|
||||
<button className='h-14 w-14 rounded-full dark:bg-white/10'></button>
|
||||
{/* Menu */}
|
||||
<button className='mr-2.5 flex h-14 w-14 items-center justify-center rounded-full text-white/40 transition duration-400 hover:text-white/70 dark:bg-white/10 hover:dark:bg-white/30'>
|
||||
<Icon name='more' className='h-7 w-7' />
|
||||
</button>
|
||||
|
||||
{/* Like */}
|
||||
<button
|
||||
onClick={() => likeArtist.mutateAsync(id)}
|
||||
className='flex h-14 w-14 items-center justify-center rounded-full text-white/40 transition duration-400 hover:text-white/70 dark:bg-white/10 hover:dark:bg-white/30'
|
||||
>
|
||||
<Icon
|
||||
name={isLiked ? 'heart' : 'heart-outline'}
|
||||
className='h-7 w-7'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button className='h-14 w-[125px] rounded-full dark:bg-brand-700 lg:w-[170px]'></button>
|
||||
|
||||
{/* Listen */}
|
||||
<button
|
||||
onClick={() => player.playArtistPopularTracks(id)}
|
||||
className='h-14 rounded-full px-10 text-18 font-medium text-white dark:bg-brand-700'
|
||||
>
|
||||
Listen
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import useIsMobile from '@/web/hooks/useIsMobile'
|
||||
import useAppleMusicArtist from '@/web/hooks/useAppleMusicArtist'
|
||||
import { cx, css } from '@emotion/css'
|
||||
|
||||
const ArtistInfo = ({ artist }: { artist?: Artist }) => {
|
||||
const isMobile = useIsMobile()
|
||||
|
|
@ -11,20 +12,27 @@ const ArtistInfo = ({ artist }: { artist?: Artist }) => {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className='text-28 font-semibold text-night-50 lg:text-32'>
|
||||
<div className='text-28 font-semibold text-white/70 lg:text-32'>
|
||||
{artist?.name}
|
||||
</div>
|
||||
<div className='mt-2.5 text-24 font-medium text-night-400 lg:mt-6'>
|
||||
<div className='mt-2.5 text-24 font-medium text-white/40 lg:mt-6'>
|
||||
Artist
|
||||
</div>
|
||||
<div className='mt-1 text-12 font-medium text-night-400'>
|
||||
<div className='mt-1 text-12 font-medium text-white/40'>
|
||||
{artist?.musicSize} Tracks · {artist?.albumSize} Albums ·{' '}
|
||||
{artist?.mvSize} Videos
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{!isMobile && !isLoadingArtistFromApple && (
|
||||
<div className='line-clamp-5 mt-6 text-14 font-bold text-night-400'>
|
||||
<div
|
||||
className={cx(
|
||||
'line-clamp-5 mt-6 text-14 font-bold text-white/40',
|
||||
css`
|
||||
height: 86px;
|
||||
`
|
||||
)}
|
||||
>
|
||||
{artistFromApple?.attributes?.artistBio || artist?.briefDesc}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
101
packages/web/pages/New/Artist/Header/Cover.tsx
Normal file
101
packages/web/pages/New/Artist/Header/Cover.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { isIOS, isSafari, resizeImage } from '@/web/utils/common'
|
||||
import Image from '@/web/components/New/Image'
|
||||
import { cx, css } from '@emotion/css'
|
||||
import useAppleMusicArtist from '@/web/hooks/useAppleMusicArtist'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import Hls from 'hls.js'
|
||||
import { motion } from 'framer-motion'
|
||||
import uiStates from '@/web/states/uiStates'
|
||||
|
||||
const VideoCover = ({ source }: { source?: string }) => {
|
||||
const ref = useRef<HTMLVideoElement>(null)
|
||||
const hls = useRef<Hls>(new Hls())
|
||||
|
||||
useEffect(() => {
|
||||
if (source && Hls.isSupported()) {
|
||||
const video = document.querySelector('#video-cover') as HTMLVideoElement
|
||||
hls.current.loadSource(source)
|
||||
hls.current.attachMedia(video)
|
||||
}
|
||||
}, [source])
|
||||
|
||||
return (
|
||||
<div className='z-10 aspect-square overflow-hidden rounded-24'>
|
||||
<video
|
||||
id='video-cover'
|
||||
className='h-full w-full'
|
||||
ref={ref}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Cover = ({ artist }: { artist?: Artist }) => {
|
||||
const { data: artistFromApple, isLoading: isLoadingArtistFromApple } =
|
||||
useAppleMusicArtist({
|
||||
id: artist?.id,
|
||||
name: artist?.name,
|
||||
})
|
||||
|
||||
const video =
|
||||
artistFromApple?.attributes?.editorialVideo?.motionArtistSquare1x1?.video
|
||||
const cover = isLoadingArtistFromApple
|
||||
? ''
|
||||
: artistFromApple?.attributes?.artwork?.url || artist?.img1v1Url
|
||||
|
||||
useEffect(() => {
|
||||
if (cover) uiStates.blurBackgroundImage = cover
|
||||
}, [cover])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cx(
|
||||
'relative',
|
||||
css`
|
||||
grid-area: cover;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
className='aspect-square h-full w-full lg:z-10 lg:rounded-24'
|
||||
src={resizeImage(
|
||||
isLoadingArtistFromApple
|
||||
? ''
|
||||
: artistFromApple?.attributes?.artwork?.url ||
|
||||
artist?.img1v1Url ||
|
||||
'',
|
||||
'lg'
|
||||
)}
|
||||
/>
|
||||
|
||||
{video && (
|
||||
<motion.div
|
||||
initial={{ opacity: isIOS ? 1 : 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className='absolute inset-0 z-10 h-full w-full'
|
||||
>
|
||||
{isSafari ? (
|
||||
<video
|
||||
src={video}
|
||||
className='h-full w-full'
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
></video>
|
||||
) : (
|
||||
<VideoCover source={video} />
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cover
|
||||
|
|
@ -1,19 +1,10 @@
|
|||
import { resizeImage } from '@/web/utils/common'
|
||||
import { cx, css } from '@emotion/css'
|
||||
import Image from '@/web/components/New/Image'
|
||||
import { breakpoint as bp } from '@/web/utils/const'
|
||||
import BlurBackground from '@/web/components/New/BlurBackground'
|
||||
import ArtistInfo from './ArtistInfo'
|
||||
import Actions from './Actions'
|
||||
import LatestRelease from './LatestRelease'
|
||||
import useAppleMusicArtist from '@/web/hooks/useAppleMusicArtist'
|
||||
|
||||
import Cover from "./Cover"
|
||||
const Header = ({ artist }: { artist?: Artist }) => {
|
||||
const { data: artistFromApple, isLoading: isLoadingArtistFromApple } =
|
||||
useAppleMusicArtist({
|
||||
id: artist?.id,
|
||||
name: artist?.name,
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -27,30 +18,7 @@ const Header = ({ artist }: { artist?: Artist }) => {
|
|||
`
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
className={cx(
|
||||
'aspect-square lg:z-10 lg:rounded-24',
|
||||
css`
|
||||
grid-area: cover;
|
||||
`
|
||||
)}
|
||||
src={resizeImage(
|
||||
isLoadingArtistFromApple
|
||||
? ''
|
||||
: artistFromApple?.attributes?.artwork?.url ||
|
||||
artist?.img1v1Url ||
|
||||
'',
|
||||
'lg'
|
||||
)}
|
||||
/>
|
||||
|
||||
<BlurBackground
|
||||
cover={
|
||||
isLoadingArtistFromApple
|
||||
? ''
|
||||
: artistFromApple?.attributes?.artwork?.url || artist?.img1v1Url
|
||||
}
|
||||
/>
|
||||
<Cover artist={artist} />
|
||||
|
||||
<div
|
||||
className={cx(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useNavigate, useParams } from 'react-router-dom'
|
|||
import Image from '@/web/components/New/Image'
|
||||
import useArtistAlbums from '@/web/api/hooks/useArtistAlbums'
|
||||
import { useMemo } from 'react'
|
||||
import useArtistMV from '@/web/api/hooks/useArtistMV'
|
||||
|
||||
const Album = () => {
|
||||
const params = useParams()
|
||||
|
|
@ -54,30 +55,40 @@ const Album = () => {
|
|||
}
|
||||
|
||||
const Video = () => {
|
||||
const params = useParams()
|
||||
const { data: videos } = useArtistMV({ id: Number(params.id) || 0 })
|
||||
const video = videos?.mvs?.[0]
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className='mt-4 flex rounded-24 bg-white/10 p-2.5'>
|
||||
<Image
|
||||
src={resizeImage(
|
||||
'https://p1.music.126.net/am47BH30IGQit_L2vYaArg==/109951167502760845.jpg',
|
||||
'sm'
|
||||
)}
|
||||
className={cx(
|
||||
css`
|
||||
height: 60px;
|
||||
width: 106px;
|
||||
border-radius: 16px;
|
||||
`
|
||||
)}
|
||||
/>
|
||||
<div className='flex-shrink-1 ml-2'>
|
||||
<div className='line-clamp-2 text-16 font-medium text-night-100'>
|
||||
Swedish House Mafia & The Weeknd Live at C...
|
||||
<>
|
||||
{video && (
|
||||
<div
|
||||
className='mt-4 flex rounded-24 bg-white/10 p-2.5'
|
||||
onClick={() => navigate(`/mv/${video.id}`)}
|
||||
>
|
||||
<img
|
||||
src={video.imgurl16v9}
|
||||
className={cx(
|
||||
'object-contain',
|
||||
css`
|
||||
height: 60px;
|
||||
border-radius: 16px;
|
||||
`
|
||||
)}
|
||||
/>
|
||||
<div className='flex-shrink-1 ml-2'>
|
||||
<div className='line-clamp-1 text-16 font-medium text-night-100'>
|
||||
{video.name}
|
||||
</div>
|
||||
<div className='mt-1 text-14 font-bold text-night-500'>MV</div>
|
||||
<div className='mt-1.5 text-12 font-medium text-night-500'>
|
||||
{dayjs(video.publishTime).format('MMM DD, YYYY')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-1.5 text-12 font-medium text-night-500'>
|
||||
{dayjs().format('MMM DD, YYYY')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
} from '@/web/api/playlist'
|
||||
import { PlaylistApiNames } from '@/shared/api/Playlists'
|
||||
import { useState } from 'react'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import CoverRowVirtual from '@/web/components/New/CoverRowVirtual'
|
||||
import PageTransition from '@/web/components/New/PageTransition'
|
||||
import { playerWidth, topbarHeight } from '@/web/utils/const'
|
||||
|
|
@ -20,31 +20,27 @@ const reactQueryOptions = {
|
|||
}
|
||||
|
||||
const Recommend = () => {
|
||||
const { data: dailyRecommendPlaylists } = useQuery(
|
||||
PlaylistApiNames.FetchDailyRecommendPlaylists,
|
||||
const { data: dailyRecommendPlaylists, isLoading: isLoadingDaily } = useQuery(
|
||||
[PlaylistApiNames.FetchDailyRecommendPlaylists],
|
||||
() => fetchDailyRecommendPlaylists(),
|
||||
reactQueryOptions
|
||||
)
|
||||
const { data: recommendedPlaylists } = useQuery(
|
||||
const { data: recommendedPlaylists, isLoading: isLoading } = useQuery(
|
||||
[PlaylistApiNames.FetchRecommendedPlaylists, { limit: 200 }],
|
||||
() => fetchRecommendedPlaylists({ limit: 200 }),
|
||||
reactQueryOptions
|
||||
)
|
||||
const playlists = [
|
||||
...(dailyRecommendPlaylists?.recommend || []),
|
||||
...(recommendedPlaylists?.result || []),
|
||||
]
|
||||
const playlists =
|
||||
isLoadingDaily || isLoading
|
||||
? []
|
||||
: [
|
||||
...(dailyRecommendPlaylists?.recommend || []),
|
||||
...(recommendedPlaylists?.result || []),
|
||||
]
|
||||
|
||||
// return (
|
||||
// <CoverRowVirtual
|
||||
// playlists={playlists}
|
||||
// containerStyle={{
|
||||
// height: `${document.body.clientHeight - topbarHeight - 44}px`,
|
||||
// }}
|
||||
// />
|
||||
// )
|
||||
return <CoverRowVirtual playlists={playlists} />
|
||||
|
||||
return <CoverRow playlists={playlists} />
|
||||
// return <CoverRow playlists={playlists} />
|
||||
}
|
||||
|
||||
const All = () => {
|
||||
|
|
@ -66,27 +62,29 @@ const Browse = () => {
|
|||
|
||||
return (
|
||||
<PageTransition>
|
||||
{/* Topbar background */}
|
||||
<div
|
||||
className={cx(
|
||||
'pointer-events-none fixed top-0 left-0 z-10 hidden lg:block',
|
||||
css`
|
||||
height: 230px;
|
||||
right: ${playerWidth + 32}px;
|
||||
background-image: url(${topbarBackground});
|
||||
`
|
||||
)}
|
||||
></div>
|
||||
<div className='relative'>
|
||||
{/* Topbar background */}
|
||||
<div
|
||||
className={cx(
|
||||
'pointer-events-none fixed top-0 left-10 z-10 hidden lg:block',
|
||||
css`
|
||||
height: 230px;
|
||||
right: ${playerWidth + 32}px;
|
||||
background-image: url(${topbarBackground});
|
||||
`
|
||||
)}
|
||||
></div>
|
||||
|
||||
<Tabs
|
||||
tabs={categories}
|
||||
value={active}
|
||||
onChange={category => setActive(category)}
|
||||
className='sticky top-0 z-10 mt-2.5 px-2.5 lg:mt-0 lg:px-0'
|
||||
/>
|
||||
<Tabs
|
||||
tabs={categories}
|
||||
value={active}
|
||||
onChange={category => setActive(category)}
|
||||
className='absolute top-0 z-10 mt-2.5 px-2.5 lg:mt-0 lg:px-0'
|
||||
/>
|
||||
|
||||
<div className='relative mx-2.5 mt-5 lg:mx-0'>
|
||||
{categories.find(c => c.id === active)?.component}
|
||||
<div className='absolute inset-0 mx-2.5 mt-5 lg:mx-0 lg:mt-0'>
|
||||
{categories.find(c => c.id === active)?.component}
|
||||
</div>
|
||||
</div>
|
||||
</PageTransition>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { fetchTracksWithReactQuery } from '@/web/api/hooks/useTracks'
|
|||
import { useEffect, useState } from 'react'
|
||||
import { sampleSize } from 'lodash-es'
|
||||
import { FetchPlaylistResponse } from '@/shared/api/Playlists'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
interface DiscoverAlbum {
|
||||
id: number
|
||||
|
|
|
|||
62
packages/web/pages/New/MV.tsx
Normal file
62
packages/web/pages/New/MV.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import PageTransition from '@/web/components/New/PageTransition'
|
||||
import useMV, { useMVUrl } from '@/web/api/hooks/useMV'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import Plyr, { PlyrOptions, PlyrSource } from 'plyr-react'
|
||||
import 'plyr-react/plyr.css'
|
||||
import { useMemo } from 'react'
|
||||
import { css, cx } from '@emotion/css'
|
||||
|
||||
const plyrStyle = css`
|
||||
--plyr-color-main: rgb(152 208 11);
|
||||
--plyr-video-control-background-hover: rgba(255, 255, 255, 0.3);
|
||||
--plyr-control-radius: 8px;
|
||||
--plyr-range-fill-background: white;
|
||||
button[data-plyr='play']:not(.plyr__controls__item) {
|
||||
--plyr-video-control-background-hover: var(--plyr-color-main);
|
||||
}
|
||||
`
|
||||
|
||||
const plyrOptions: PlyrOptions = {
|
||||
settings: [],
|
||||
controls: [
|
||||
'play-large',
|
||||
'play',
|
||||
'progress',
|
||||
'current-time',
|
||||
'mute',
|
||||
'volume',
|
||||
'fullscreen',
|
||||
],
|
||||
resetOnEnd: true,
|
||||
ratio: '16:9',
|
||||
}
|
||||
|
||||
const MV = () => {
|
||||
const params = useParams()
|
||||
const { data: mv } = useMV({ mvid: Number(params.id) || 0 })
|
||||
const { data: mvUrl } = useMVUrl({ id: Number(params.id) || 0 })
|
||||
const source: PlyrSource = useMemo(
|
||||
() => ({
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: mvUrl?.data?.url || '',
|
||||
},
|
||||
],
|
||||
poster: mv?.data.cover,
|
||||
title: mv?.data.name,
|
||||
}),
|
||||
[mv?.data.cover, mv?.data.name, mvUrl?.data?.url]
|
||||
)
|
||||
|
||||
return (
|
||||
<PageTransition>
|
||||
<div className='text-white'>{mv?.data.name}</div>
|
||||
<div className={cx('aspect-video overflow-hidden rounded-24', plyrStyle)}>
|
||||
{mvUrl && <Plyr options={plyrOptions} source={source} />}
|
||||
</div>
|
||||
</PageTransition>
|
||||
)
|
||||
}
|
||||
|
||||
export default MV
|
||||
|
|
@ -7,6 +7,7 @@ import useUserPlaylists from '@/web/api/hooks/useUserPlaylists'
|
|||
import useUserAlbums from '@/web/api/hooks/useUserAlbums'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import uiStates from '@/web/states/uiStates'
|
||||
import ArtistRow from '@/web/components/New/ArtistRow'
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
|
|
@ -30,17 +31,21 @@ const tabs = [
|
|||
const Albums = () => {
|
||||
const { data: albums } = useUserAlbums()
|
||||
|
||||
return <CoverRow albums={albums?.data} className='mt-6 px-2.5 lg:px-0' />
|
||||
return <CoverRow albums={albums?.data} />
|
||||
}
|
||||
|
||||
const Playlists = () => {
|
||||
const { data: playlists } = useUserPlaylists()
|
||||
const p = useMemo(() => playlists?.playlist?.slice(1), [playlists])
|
||||
return <CoverRow playlists={p} className='mt-6 px-2.5 lg:px-0' />
|
||||
return <CoverRow playlists={p} />
|
||||
}
|
||||
|
||||
const Artists = () => {
|
||||
const { data: artists } = useUserArtists()
|
||||
return <ArtistRow artists={artists?.data || []} />
|
||||
}
|
||||
|
||||
const Collections = () => {
|
||||
// const { data: artists } = useUserArtists()
|
||||
const { librarySelectedTab: selectedTab } = useSnapshot(uiStates)
|
||||
const setSelectedTab = (
|
||||
id: 'playlists' | 'albums' | 'artists' | 'videos'
|
||||
|
|
@ -56,8 +61,11 @@ const Collections = () => {
|
|||
onChange={(id: string) => setSelectedTab(id)}
|
||||
className='px-2.5 lg:px-0'
|
||||
/>
|
||||
{selectedTab === 'albums' && <Albums />}
|
||||
{selectedTab === 'playlists' && <Playlists />}
|
||||
<div className='mt-6 px-2.5 lg:px-0'>
|
||||
{selectedTab === 'albums' && <Albums />}
|
||||
{selectedTab === 'playlists' && <Playlists />}
|
||||
{selectedTab === 'artists' && <Artists />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ import { lyricParser } from '@/web/utils/lyric'
|
|||
import Image from '@/web/components/New/Image'
|
||||
import { resizeImage } from '@/web/utils/common'
|
||||
import { breakpoint as bp } from '@/web/utils/const'
|
||||
import useUser from '@/web/api/hooks/useUser'
|
||||
|
||||
const Lyrics = ({ tracksIDs }: { tracksIDs: number[] }) => {
|
||||
const [id, setId] = useState(0)
|
||||
const { data: user } = useUser()
|
||||
|
||||
useEffect(() => {
|
||||
if (id === 0) {
|
||||
|
|
@ -37,7 +39,7 @@ const Lyrics = ({ tracksIDs }: { tracksIDs: number[] }) => {
|
|||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'line-clamp-4 text-18 font-medium text-white/20 lg:text-21',
|
||||
'line-clamp-5',
|
||||
css`
|
||||
height: 86px;
|
||||
${bp.lg} {
|
||||
|
|
@ -46,8 +48,16 @@ const Lyrics = ({ tracksIDs }: { tracksIDs: number[] }) => {
|
|||
`
|
||||
)}
|
||||
>
|
||||
<div className='mb-3.5 text-18 font-medium text-white/70'>
|
||||
{user?.profile?.nickname}'S LIKED TRACKS
|
||||
</div>
|
||||
{lyricLines.map((line, index) => (
|
||||
<div key={`${index}-${line}`}>{line}</div>
|
||||
<div
|
||||
key={`${index}-${line}`}
|
||||
className='text-18 font-medium text-white/20'
|
||||
>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -136,7 +146,7 @@ const PlayLikedSongsCard = () => {
|
|||
navigate(`/playlist/${likedSongsPlaylist?.playlist.id}`)
|
||||
}
|
||||
className={cx(
|
||||
'flex items-center justify-center rounded-full bg-white/10 text-night-400',
|
||||
'flex items-center justify-center rounded-full bg-white/10 text-night-400 transition duration-400 hover:bg-white/20 hover:text-neutral-300',
|
||||
css`
|
||||
padding: 15.5px;
|
||||
`
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
import TrackListHeader from '@/web/components/New/TrackListHeader'
|
||||
import { NavLink, useParams } from 'react-router-dom'
|
||||
import PageTransition from '@/web/components/New/PageTransition'
|
||||
import TrackList from '@/web/components/New/TrackList'
|
||||
import player from '@/web/states/player'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { memo, useEffect, useMemo } from 'react'
|
||||
import usePlaylist from '@/web/api/hooks/usePlaylist'
|
||||
import useTracksInfinite from '@/web/api/hooks/useTracksInfinite'
|
||||
|
||||
const Playlist = () => {
|
||||
const params = useParams()
|
||||
const { data: playlist, isLoading } = usePlaylist({
|
||||
id: Number(params.id),
|
||||
})
|
||||
|
||||
const playerSnapshot = useSnapshot(player)
|
||||
const onPlay = async (trackID: number | null = null) => {
|
||||
if (!playlist?.playlist.id) {
|
||||
toast('无法播放歌单,该歌单不存在')
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
playerSnapshot.trackListSource?.type === 'playlist' &&
|
||||
playerSnapshot.trackListSource?.id === playlist.playlist.id &&
|
||||
playlist?.playlist?.trackIds?.[0].id
|
||||
) {
|
||||
await player.playTrack(trackID ?? playlist.playlist.trackIds[0].id)
|
||||
return
|
||||
}
|
||||
await player.playPlaylist(playlist.playlist.id, trackID)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageTransition>
|
||||
<TrackListHeader
|
||||
playlist={playlist?.playlist}
|
||||
onPlay={onPlay}
|
||||
className='mt-2.5 lg:mt-0'
|
||||
/>
|
||||
<TrackList
|
||||
tracks={playlist?.playlist?.tracks ?? []}
|
||||
onPlay={onPlay}
|
||||
className='z-10 mt-10'
|
||||
/>
|
||||
</PageTransition>
|
||||
)
|
||||
}
|
||||
|
||||
export default Playlist
|
||||
75
packages/web/pages/New/Playlist/Header.tsx
Normal file
75
packages/web/pages/New/Playlist/Header.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import usePlaylist from '@/web/api/hooks/usePlaylist'
|
||||
import useUser from '@/web/api/hooks/useUser'
|
||||
import useUserPlaylists, {
|
||||
useMutationLikeAPlaylist,
|
||||
} from '@/web/api/hooks/useUserPlaylists'
|
||||
import TrackListHeader from '@/web/components/New/TrackListHeader'
|
||||
import player from '@/web/states/player'
|
||||
import { formatDate } from '@/web/utils/common'
|
||||
import { useMemo } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
const Header = () => {
|
||||
const params = useParams()
|
||||
|
||||
const { data: playlistRaw, isLoading } = usePlaylist({
|
||||
id: Number(params.id),
|
||||
})
|
||||
const { data: user } = useUser()
|
||||
|
||||
const { data: userLikedPlaylists } = useUserPlaylists()
|
||||
|
||||
const playlist = playlistRaw?.playlist
|
||||
|
||||
// For <Cover />
|
||||
const cover = playlist?.coverImgUrl || playlist?.picUrl
|
||||
|
||||
// For <Info />
|
||||
const title = playlist?.name
|
||||
const creatorName = playlist?.creator?.nickname
|
||||
const creatorLink = undefined // TODO: 链接到用户页面
|
||||
const description = playlist?.description || ''
|
||||
const extraInfo = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
Updated at {formatDate(playlist?.updateTime || 0, 'en')} ·{' '}
|
||||
{playlist?.trackCount} tracks
|
||||
</>
|
||||
)
|
||||
}, [playlist])
|
||||
|
||||
// For <Actions />
|
||||
const isLiked = useMemo(() => {
|
||||
const id = Number(params.id)
|
||||
if (!id) return false
|
||||
return !!userLikedPlaylists?.playlist.find(p => p.id === id)
|
||||
}, [params.id, userLikedPlaylists?.playlist])
|
||||
|
||||
const onPlay = async (trackID: number | null = null) => {
|
||||
await player.playPlaylist(playlist?.id, trackID)
|
||||
}
|
||||
|
||||
const likeAPlaylist = useMutationLikeAPlaylist()
|
||||
const onLike = async () => {
|
||||
likeAPlaylist.mutateAsync(playlist?.id || Number(params.id))
|
||||
}
|
||||
|
||||
return (
|
||||
<TrackListHeader
|
||||
{...{
|
||||
title,
|
||||
creatorName,
|
||||
creatorLink,
|
||||
description,
|
||||
extraInfo,
|
||||
cover,
|
||||
isLiked,
|
||||
onLike:
|
||||
user?.account?.id === playlist?.creator?.userId ? undefined : onLike,
|
||||
onPlay,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
30
packages/web/pages/New/Playlist/Playlist.tsx
Normal file
30
packages/web/pages/New/Playlist/Playlist.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { useParams } from 'react-router-dom'
|
||||
import PageTransition from '@/web/components/New/PageTransition'
|
||||
import TrackList from '@/web/components/New/TrackList'
|
||||
import player from '@/web/states/player'
|
||||
import usePlaylist from '@/web/api/hooks/usePlaylist'
|
||||
import Header from './Header'
|
||||
|
||||
const Playlist = () => {
|
||||
const params = useParams()
|
||||
const { data: playlist } = usePlaylist({
|
||||
id: Number(params.id),
|
||||
})
|
||||
|
||||
const onPlay = async (trackID: number | null = null) => {
|
||||
await player.playPlaylist(playlist?.playlist?.id, trackID)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageTransition>
|
||||
<Header />
|
||||
<TrackList
|
||||
tracks={playlist?.playlist?.tracks ?? []}
|
||||
onPlay={onPlay}
|
||||
className='z-10 mt-10'
|
||||
/>
|
||||
</PageTransition>
|
||||
)
|
||||
}
|
||||
|
||||
export default Playlist
|
||||
3
packages/web/pages/New/Playlist/index.tsx
Normal file
3
packages/web/pages/New/Playlist/index.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Playlist from './Playlist'
|
||||
|
||||
export default Playlist
|
||||
Loading…
Add table
Add a link
Reference in a new issue