mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-16 13:17:46 +00:00
feat: updates
This commit is contained in:
parent
8f4c3d8e5b
commit
f340a90117
34 changed files with 781 additions and 323 deletions
94
packages/web/pages/New/Browse.tsx
Normal file
94
packages/web/pages/New/Browse.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import Tabs from '@/web/components/New/Tabs'
|
||||
import {
|
||||
fetchDailyRecommendPlaylists,
|
||||
fetchRecommendedPlaylists,
|
||||
} from '@/web/api/playlist'
|
||||
import { PlaylistApiNames } from '@/shared/api/Playlists'
|
||||
import { useState } from 'react'
|
||||
import { useQuery } from 'react-query'
|
||||
import CoverRowVirtual from '@/web/components/New/CoverRowVirtual'
|
||||
import PageTransition from '@/web/components/New/PageTransition'
|
||||
import { playerWidth, topbarHeight } from '@/web/utils/const'
|
||||
import { cx, css } from '@emotion/css'
|
||||
import CoverRow from '@/web/components/New/CoverRow'
|
||||
import topbarBackground from '@/web/assets/images/topbar-background.png'
|
||||
|
||||
const reactQueryOptions = {
|
||||
refetchOnWindowFocus: false,
|
||||
refetchInterval: 1000 * 60 * 60, // 1 hour
|
||||
}
|
||||
|
||||
const Recommend = () => {
|
||||
const { data: dailyRecommendPlaylists } = useQuery(
|
||||
PlaylistApiNames.FetchDailyRecommendPlaylists,
|
||||
() => fetchDailyRecommendPlaylists(),
|
||||
reactQueryOptions
|
||||
)
|
||||
const { data: recommendedPlaylists } = useQuery(
|
||||
[PlaylistApiNames.FetchRecommendedPlaylists, { limit: 200 }],
|
||||
() => fetchRecommendedPlaylists({ limit: 200 }),
|
||||
reactQueryOptions
|
||||
)
|
||||
const playlists = [
|
||||
...(dailyRecommendPlaylists?.recommend || []),
|
||||
...(recommendedPlaylists?.result || []),
|
||||
]
|
||||
|
||||
// return (
|
||||
// <CoverRowVirtual
|
||||
// playlists={playlists}
|
||||
// containerStyle={{
|
||||
// height: `${document.body.clientHeight - topbarHeight - 44}px`,
|
||||
// }}
|
||||
// />
|
||||
// )
|
||||
|
||||
return <CoverRow playlists={playlists} />
|
||||
}
|
||||
|
||||
const All = () => {
|
||||
return <div></div>
|
||||
}
|
||||
|
||||
const categories = [
|
||||
{ id: 'recommend', name: 'Recommend', component: <Recommend /> },
|
||||
{ id: 'all', name: 'All', component: <All /> },
|
||||
{ id: 'featured', name: 'Featured', component: <Recommend /> },
|
||||
{ id: 'official', name: 'Official', component: <Recommend /> },
|
||||
{ id: 'charts', name: 'Charts', component: <Recommend /> },
|
||||
]
|
||||
const categoriesKeys = categories.map(c => c.id)
|
||||
type Key = typeof categoriesKeys[number]
|
||||
|
||||
const Browse = () => {
|
||||
const [active, setActive] = useState<Key>('recommend')
|
||||
|
||||
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>
|
||||
|
||||
<Tabs
|
||||
tabs={categories}
|
||||
value={active}
|
||||
onChange={category => setActive(category)}
|
||||
className='sticky top-0 z-10 px-2.5 lg:px-0'
|
||||
/>
|
||||
|
||||
<div className='relative mx-2.5 mt-5 lg:mx-0'>
|
||||
{categories.find(c => c.id === active)?.component}
|
||||
</div>
|
||||
</PageTransition>
|
||||
)
|
||||
}
|
||||
|
||||
export default Browse
|
||||
|
|
@ -102,7 +102,7 @@ const Discover = () => {
|
|||
|
||||
return (
|
||||
<PageTransition disableEnterAnimation={true}>
|
||||
<div className='mx-2.5 lg:mx-0'>
|
||||
<div className='mx-2.5 pb-10 lg:mx-0 lg:pb-16'>
|
||||
<CoverWall albums={albums} />
|
||||
</div>
|
||||
</PageTransition>
|
||||
|
|
|
|||
|
|
@ -1,115 +0,0 @@
|
|||
import { css, cx } from '@emotion/css'
|
||||
import PlayLikedSongsCard from '@/web/components/New/PlayLikedSongsCard'
|
||||
import PageTransition from '@/web/components/New/PageTransition'
|
||||
import useUserArtists from '@/web/api/hooks/useUserArtists'
|
||||
import ArtistRow from '@/web/components/New/ArtistRow'
|
||||
import Tabs from '@/web/components/New/Tabs'
|
||||
import { useMemo, useState } from 'react'
|
||||
import CoverRow from '@/web/components/New/CoverRow'
|
||||
import useUserPlaylists from '@/web/api/hooks/useUserPlaylists'
|
||||
import useUserAlbums from '@/web/api/hooks/useUserAlbums'
|
||||
import useUserListenedRecords from '@/web/api/hooks/useUserListenedRecords'
|
||||
import useArtists from '@/web/api/hooks/useArtists'
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: 'playlists',
|
||||
name: 'Playlists',
|
||||
},
|
||||
{
|
||||
id: 'albums',
|
||||
name: 'Albums',
|
||||
},
|
||||
{
|
||||
id: 'artists',
|
||||
name: 'Artists',
|
||||
},
|
||||
{
|
||||
id: 'videos',
|
||||
name: 'Videos',
|
||||
},
|
||||
]
|
||||
|
||||
const Albums = () => {
|
||||
const { data: albums } = useUserAlbums()
|
||||
|
||||
return <CoverRow albums={albums?.data} className='mt-6 px-2.5 lg:px-0' />
|
||||
}
|
||||
|
||||
const Playlists = () => {
|
||||
const { data: playlists } = useUserPlaylists()
|
||||
return (
|
||||
<CoverRow
|
||||
playlists={playlists?.playlist?.slice(1)}
|
||||
className='mt-6 px-2.5 lg:px-0'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Collections = () => {
|
||||
// const { data: artists } = useUserArtists()
|
||||
const [selectedTab, setSelectedTab] = useState(tabs[0].id)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tabs
|
||||
tabs={tabs}
|
||||
value={selectedTab}
|
||||
onChange={(id: string) => setSelectedTab(id)}
|
||||
className='px-2.5 lg:px-0'
|
||||
/>
|
||||
{selectedTab === 'albums' && <Albums />}
|
||||
{selectedTab === 'playlists' && <Playlists />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const RecentlyListened = () => {
|
||||
const { data: listenedRecords } = useUserListenedRecords({ type: 'week' })
|
||||
const recentListenedArtistsIDs = useMemo(() => {
|
||||
const artists: {
|
||||
id: number
|
||||
playCount: number
|
||||
}[] = []
|
||||
listenedRecords?.weekData?.forEach(record => {
|
||||
const artist = record.song.ar[0]
|
||||
const index = artists.findIndex(a => a.id === artist.id)
|
||||
if (index === -1) {
|
||||
artists.push({
|
||||
id: artist.id,
|
||||
playCount: record.playCount,
|
||||
})
|
||||
} else {
|
||||
artists[index].playCount += record.playCount
|
||||
}
|
||||
})
|
||||
|
||||
return artists
|
||||
.sort((a, b) => b.playCount - a.playCount)
|
||||
.slice(0, 5)
|
||||
.map(artist => artist.id)
|
||||
}, [listenedRecords])
|
||||
const { data: recentListenedArtists } = useArtists(recentListenedArtistsIDs)
|
||||
|
||||
return (
|
||||
<ArtistRow
|
||||
artists={recentListenedArtists?.map(a => a.artist)}
|
||||
placeholderRow={1}
|
||||
title='RECENTLY LISTENED'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const My = () => {
|
||||
return (
|
||||
<PageTransition>
|
||||
<div className='grid grid-cols-1 gap-10'>
|
||||
<PlayLikedSongsCard />
|
||||
<RecentlyListened />
|
||||
<Collections />
|
||||
</div>
|
||||
</PageTransition>
|
||||
)
|
||||
}
|
||||
|
||||
export default My
|
||||
58
packages/web/pages/New/My/Collections.tsx
Normal file
58
packages/web/pages/New/My/Collections.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { css, cx } from '@emotion/css'
|
||||
import useUserArtists from '@/web/api/hooks/useUserArtists'
|
||||
import Tabs from '@/web/components/New/Tabs'
|
||||
import { useMemo, useState } from 'react'
|
||||
import CoverRow from '@/web/components/New/CoverRow'
|
||||
import useUserPlaylists from '@/web/api/hooks/useUserPlaylists'
|
||||
import useUserAlbums from '@/web/api/hooks/useUserAlbums'
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: 'playlists',
|
||||
name: 'Playlists',
|
||||
},
|
||||
{
|
||||
id: 'albums',
|
||||
name: 'Albums',
|
||||
},
|
||||
{
|
||||
id: 'artists',
|
||||
name: 'Artists',
|
||||
},
|
||||
{
|
||||
id: 'videos',
|
||||
name: 'Videos',
|
||||
},
|
||||
]
|
||||
|
||||
const Albums = () => {
|
||||
const { data: albums } = useUserAlbums()
|
||||
|
||||
return <CoverRow albums={albums?.data} className='mt-6 px-2.5 lg:px-0' />
|
||||
}
|
||||
|
||||
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' />
|
||||
}
|
||||
|
||||
const Collections = () => {
|
||||
// const { data: artists } = useUserArtists()
|
||||
const [selectedTab, setSelectedTab] = useState(tabs[0].id)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tabs
|
||||
tabs={tabs}
|
||||
value={selectedTab}
|
||||
onChange={(id: string) => setSelectedTab(id)}
|
||||
className='px-2.5 lg:px-0'
|
||||
/>
|
||||
{selectedTab === 'albums' && <Albums />}
|
||||
{selectedTab === 'playlists' && <Playlists />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Collections
|
||||
19
packages/web/pages/New/My/My.tsx
Normal file
19
packages/web/pages/New/My/My.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { css, cx } from '@emotion/css'
|
||||
import PlayLikedSongsCard from './PlayLikedSongsCard'
|
||||
import PageTransition from '@/web/components/New/PageTransition'
|
||||
import RecentlyListened from './RecentlyListened'
|
||||
import Collections from './Collections'
|
||||
|
||||
const My = () => {
|
||||
return (
|
||||
<PageTransition>
|
||||
<div className='grid grid-cols-1 gap-10'>
|
||||
<PlayLikedSongsCard />
|
||||
<RecentlyListened />
|
||||
<Collections />
|
||||
</div>
|
||||
</PageTransition>
|
||||
)
|
||||
}
|
||||
|
||||
export default My
|
||||
152
packages/web/pages/New/My/PlayLikedSongsCard.tsx
Normal file
152
packages/web/pages/New/My/PlayLikedSongsCard.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import useLyric from '@/web/api/hooks/useLyric'
|
||||
import usePlaylist from '@/web/api/hooks/usePlaylist'
|
||||
import useUserPlaylists from '@/web/api/hooks/useUserPlaylists'
|
||||
import { player } from '@/web/store'
|
||||
import { sample, chunk, sampleSize } from 'lodash-es'
|
||||
import { css, cx } from '@emotion/css'
|
||||
import { useState, useEffect, useMemo, useCallback, memo } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import Icon from '@/web/components/Icon'
|
||||
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'
|
||||
|
||||
const Lyrics = ({ tracksIDs }: { tracksIDs: number[] }) => {
|
||||
const [id, setId] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (id === 0) {
|
||||
setId(sample(tracksIDs) || 0)
|
||||
}
|
||||
}, [id, tracksIDs])
|
||||
|
||||
const { data: lyric } = useLyric({ id })
|
||||
|
||||
const lyricLines = useMemo(() => {
|
||||
if (!lyric?.lrc?.lyric) return []
|
||||
|
||||
const parsedLyrics = lyricParser(lyric)
|
||||
|
||||
const lines = parsedLyrics.lyric.map(line => line.content)
|
||||
|
||||
return sample(chunk(lines, 4)) ?? []
|
||||
}, [lyric])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'line-clamp-4 text-18 font-medium text-white/20 lg:text-21',
|
||||
css`
|
||||
height: 86px;
|
||||
${bp.lg} {
|
||||
height: auto;
|
||||
}
|
||||
`
|
||||
)}
|
||||
>
|
||||
{lyricLines.map((line, index) => (
|
||||
<div key={`${index}-${line}`}>{line}</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Covers = memo(({ tracks }: { tracks: Track[] }) => {
|
||||
return (
|
||||
<div className='mt-6 grid w-full flex-shrink-0 grid-cols-3 gap-2.5 lg:mt-0 lg:ml-8 lg:w-auto'>
|
||||
{tracks.map(track => (
|
||||
<Image
|
||||
src={resizeImage(track.al.picUrl || '', 'md')}
|
||||
className={cx(
|
||||
'aspect-square rounded-24',
|
||||
css`
|
||||
${bp.lg} {
|
||||
height: 125px;
|
||||
width: 125px;
|
||||
}
|
||||
`
|
||||
)}
|
||||
key={track.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
Covers.displayName = 'Covers'
|
||||
|
||||
const PlayLikedSongsCard = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { data: playlists } = useUserPlaylists()
|
||||
|
||||
const { data: likedSongsPlaylist } = usePlaylist({
|
||||
id: playlists?.playlist?.[0].id ?? 0,
|
||||
})
|
||||
|
||||
const handlePlay = useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
if (!likedSongsPlaylist?.playlist.id) {
|
||||
toast('无法播放歌单')
|
||||
return
|
||||
}
|
||||
player.playPlaylist(likedSongsPlaylist.playlist.id)
|
||||
},
|
||||
[likedSongsPlaylist?.playlist.id]
|
||||
)
|
||||
|
||||
const [sampledTracks, setSampledTracks] = useState<Track[]>([])
|
||||
useEffect(() => {
|
||||
const tracks = likedSongsPlaylist?.playlist?.tracks
|
||||
if (!sampledTracks.length && tracks?.length) {
|
||||
setSampledTracks(sampleSize(tracks, 3))
|
||||
}
|
||||
}, [likedSongsPlaylist?.playlist?.tracks, sampledTracks])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'mx-2.5 flex flex-col justify-between rounded-24 p-8 dark:bg-white/10 lg:mx-0',
|
||||
css`
|
||||
height: 372px;
|
||||
${bp.lg} {
|
||||
height: 322px;
|
||||
}
|
||||
`
|
||||
)}
|
||||
>
|
||||
{/* Lyrics and Covers */}
|
||||
<div className='flex flex-col justify-between lg:flex-row'>
|
||||
<Lyrics tracksIDs={sampledTracks.map(t => t.id)} />
|
||||
<Covers tracks={sampledTracks} />
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className='flex justify-between'>
|
||||
<button
|
||||
onClick={handlePlay}
|
||||
className='rounded-full bg-brand-700 py-5 px-6 text-16 font-medium text-white'
|
||||
>
|
||||
Play Now
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
navigate(`/playlist/${likedSongsPlaylist?.playlist.id}`)
|
||||
}
|
||||
className={cx(
|
||||
'flex items-center justify-center rounded-full bg-white/10 text-night-400',
|
||||
css`
|
||||
padding: 15.5px;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<Icon name='forward' className='h-7 w-7 ' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayLikedSongsCard
|
||||
42
packages/web/pages/New/My/RecentlyListened.tsx
Normal file
42
packages/web/pages/New/My/RecentlyListened.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import useUserListenedRecords from '@/web/api/hooks/useUserListenedRecords'
|
||||
import useArtists from '@/web/api/hooks/useArtists'
|
||||
import { useMemo } from 'react'
|
||||
import ArtistRow from '@/web/components/New/ArtistRow'
|
||||
|
||||
const RecentlyListened = () => {
|
||||
const { data: listenedRecords } = useUserListenedRecords({ type: 'week' })
|
||||
const recentListenedArtistsIDs = useMemo(() => {
|
||||
const artists: {
|
||||
id: number
|
||||
playCount: number
|
||||
}[] = []
|
||||
listenedRecords?.weekData?.forEach(record => {
|
||||
const artist = record.song.ar[0]
|
||||
const index = artists.findIndex(a => a.id === artist.id)
|
||||
if (index === -1) {
|
||||
artists.push({
|
||||
id: artist.id,
|
||||
playCount: record.playCount,
|
||||
})
|
||||
} else {
|
||||
artists[index].playCount += record.playCount
|
||||
}
|
||||
})
|
||||
|
||||
return artists
|
||||
.sort((a, b) => b.playCount - a.playCount)
|
||||
.slice(0, 5)
|
||||
.map(artist => artist.id)
|
||||
}, [listenedRecords])
|
||||
const { data: recentListenedArtists } = useArtists(recentListenedArtistsIDs)
|
||||
const artist = useMemo(
|
||||
() => recentListenedArtists?.map(a => a.artist),
|
||||
[recentListenedArtists]
|
||||
)
|
||||
|
||||
return (
|
||||
<ArtistRow artists={artist} placeholderRow={1} title='RECENTLY LISTENED' />
|
||||
)
|
||||
}
|
||||
|
||||
export default RecentlyListened
|
||||
3
packages/web/pages/New/My/index.tsx
Normal file
3
packages/web/pages/New/My/index.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import My from './My'
|
||||
|
||||
export default My
|
||||
Loading…
Add table
Add a link
Reference in a new issue