feat: updates

This commit is contained in:
qier222 2022-10-28 20:29:04 +08:00
parent a1b0bcf4d3
commit 884f3df41a
No known key found for this signature in database
198 changed files with 4572 additions and 5336 deletions

View file

@ -0,0 +1,139 @@
import { css, cx } from '@emotion/css'
import useUserArtists from '@/web/api/hooks/useUserArtists'
import Tabs from '@/web/components/Tabs'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import CoverRow from '@/web/components/CoverRow'
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/ArtistRow'
import { playerWidth, topbarHeight } from '@/web/utils/const'
import topbarBackground from '@/web/assets/images/topbar-background.png'
import useIntersectionObserver from '@/web/hooks/useIntersectionObserver'
import { AnimatePresence, motion } from 'framer-motion'
import { scrollToBottom } from '@/web/utils/common'
import { throttle } from 'lodash-es'
import { useTranslation } from 'react-i18next'
const Albums = () => {
const { data: albums } = useUserAlbums()
return (
<CoverRow albums={albums?.data} itemTitle='name' itemSubtitle='artist' />
)
}
const Playlists = () => {
const { data: playlists } = useUserPlaylists()
const p = useMemo(() => playlists?.playlist?.slice(1), [playlists])
return <CoverRow playlists={p} />
}
const Artists = () => {
const { data: artists } = useUserArtists()
return <ArtistRow artists={artists?.data || []} />
}
const CollectionTabs = ({ showBg }: { showBg: boolean }) => {
const { t } = useTranslation()
const tabs = [
{
id: 'playlists',
name: t`common.playlist_other`,
},
{
id: 'albums',
name: t`common.album_other`,
},
{
id: 'artists',
name: t`common.artist_other`,
},
{
id: 'videos',
name: t`common.video_other`,
},
]
const { librarySelectedTab: selectedTab } = useSnapshot(uiStates)
const setSelectedTab = (
id: 'playlists' | 'albums' | 'artists' | 'videos'
) => {
uiStates.librarySelectedTab = id
}
return (
<>
{/* Topbar background */}
<AnimatePresence>
{showBg && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
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});
`
)}
></motion.div>
)}
</AnimatePresence>
<Tabs
tabs={tabs}
value={selectedTab}
onChange={(id: string) => {
setSelectedTab(id)
scrollToBottom(true)
}}
className={cx(
'sticky z-10 -mb-10 px-2.5 lg:px-0',
css`
top: ${topbarHeight}px;
`
)}
/>
</>
)
}
const Collections = () => {
const { librarySelectedTab: selectedTab } = useSnapshot(uiStates)
const observePoint = useRef<HTMLDivElement | null>(null)
const { onScreen: isScrollReachBottom } =
useIntersectionObserver(observePoint)
const onScroll = throttle(() => {
if (isScrollReachBottom) return
scrollToBottom(true)
}, 500)
return (
<div>
<CollectionTabs showBg={isScrollReachBottom} />
<div
className={cx(
'no-scrollbar overflow-y-auto px-2.5 pt-16 pb-16 lg:px-0',
css`
height: calc(100vh - ${topbarHeight}px);
`
)}
onScroll={onScroll}
>
{selectedTab === 'albums' && <Albums />}
{selectedTab === 'playlists' && <Playlists />}
{selectedTab === 'artists' && <Artists />}
</div>
<div ref={observePoint}></div>
</div>
)
}
export default Collections

View file

@ -0,0 +1,19 @@
import { css, cx } from '@emotion/css'
import PlayLikedSongsCard from './PlayLikedSongsCard'
import PageTransition from '@/web/components/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

View file

@ -0,0 +1,162 @@
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/states/player'
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/Image'
import { resizeImage } from '@/web/utils/common'
import { breakpoint as bp } from '@/web/utils/const'
import useUser from '@/web/api/hooks/useUser'
import { useTranslation } from 'react-i18next'
const Lyrics = ({ tracksIDs }: { tracksIDs: number[] }) => {
const { t } = useTranslation()
const [id, setId] = useState(0)
const { data: user } = useUser()
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-5',
css`
height: 86px;
${bp.lg} {
height: auto;
}
`
)}
>
<div className='mb-3.5 text-18 font-medium text-white/70'>
{t('my.xxxs-liked-tracks', { nickname: user?.profile?.nickname })}
</div>
{lyricLines.map((line, index) => (
<div
key={`${index}-${line}`}
className='text-18 font-medium text-white/20'
>
{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 { t } = useTranslation()
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()
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'
>
{t`my.playNow`}
</button>
<button
onClick={() =>
navigate(`/playlist/${likedSongsPlaylist?.playlist.id}`)
}
className={cx(
'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;
`
)}
>
<Icon name='forward' className='h-7 w-7 ' />
</button>
</div>
</div>
)
}
export default PlayLikedSongsCard

View file

@ -0,0 +1,49 @@
import useUserListenedRecords from '@/web/api/hooks/useUserListenedRecords'
import useArtists from '@/web/api/hooks/useArtists'
import { useMemo } from 'react'
import ArtistRow from '@/web/components/ArtistRow'
import { useTranslation } from 'react-i18next'
const RecentlyListened = () => {
const { t } = useTranslation()
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={t`my.recently-listened`}
/>
)
}
export default RecentlyListened

View file

@ -0,0 +1,3 @@
import My from './My'
export default My