mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-16 21:28:06 +00:00
feat: updates
This commit is contained in:
parent
cf7a4528dd
commit
0e58bb6e80
44 changed files with 1027 additions and 496 deletions
|
|
@ -2,6 +2,8 @@ import { resizeImage } from '@/web/utils/common'
|
|||
import { cx } from '@emotion/css'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import Image from './Image'
|
||||
import { prefetchAlbum } from '@/web/api/hooks/useAlbum'
|
||||
import { prefetchPlaylist } from '@/web/api/hooks/usePlaylist'
|
||||
|
||||
const CoverRow = ({
|
||||
albums,
|
||||
|
|
@ -21,6 +23,11 @@ const CoverRow = ({
|
|||
if (playlists) navigate(`/playlist/${id}`)
|
||||
}
|
||||
|
||||
const prefetch = (id: number) => {
|
||||
if (albums) prefetchAlbum({ id })
|
||||
if (playlists) prefetchPlaylist({ id })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{/* Title */}
|
||||
|
|
@ -39,6 +46,7 @@ const CoverRow = ({
|
|||
alt={album.name}
|
||||
src={resizeImage(album?.picUrl || '', 'md')}
|
||||
className='aspect-square rounded-24'
|
||||
onMouseOver={() => prefetch(album.id)}
|
||||
/>
|
||||
))}
|
||||
{playlists?.map(playlist => (
|
||||
|
|
@ -46,8 +54,12 @@ const CoverRow = ({
|
|||
onClick={() => goTo(playlist.id)}
|
||||
key={playlist.id}
|
||||
alt={playlist.name}
|
||||
src={resizeImage(playlist?.picUrl || '', 'md')}
|
||||
src={resizeImage(
|
||||
playlist.coverImgUrl || playlist?.picUrl || '',
|
||||
'md'
|
||||
)}
|
||||
className='aspect-square rounded-24'
|
||||
onMouseOver={() => prefetch(playlist.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,16 @@
|
|||
import { css, cx } from '@emotion/css'
|
||||
import { sampleSize, shuffle } from 'lodash-es'
|
||||
import Image from './Image'
|
||||
import { covers } from '@/web/.storybook/mock/tracks'
|
||||
import { resizeImage } from '@/web/utils/common'
|
||||
import useBreakpoint from '@/web/hooks/useBreakpoint'
|
||||
import { useMemo } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { prefetchAlbum } from '@/web/api/hooks/useAlbum'
|
||||
|
||||
const CoverWall = () => {
|
||||
const bigCover = useMemo(
|
||||
() =>
|
||||
shuffle(
|
||||
sampleSize([...Array(covers.length).keys()], ~~(covers.length / 3))
|
||||
),
|
||||
[]
|
||||
)
|
||||
const CoverWall = ({
|
||||
albums,
|
||||
}: {
|
||||
albums: { id: number; coverUrl: string; large: boolean }[]
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const breakpoint = useBreakpoint()
|
||||
const sizes = {
|
||||
small: {
|
||||
|
|
@ -23,7 +20,7 @@ const CoverWall = () => {
|
|||
xl: 'sm',
|
||||
'2xl': 'md',
|
||||
},
|
||||
big: {
|
||||
large: {
|
||||
sm: 'xs',
|
||||
md: 'sm',
|
||||
lg: 'md',
|
||||
|
|
@ -41,19 +38,21 @@ const CoverWall = () => {
|
|||
`
|
||||
)}
|
||||
>
|
||||
{covers.map((cover, index) => (
|
||||
{albums.map(album => (
|
||||
<Image
|
||||
src={resizeImage(
|
||||
cover,
|
||||
sizes[bigCover.includes(index) ? 'big' : 'small'][breakpoint]
|
||||
album.coverUrl,
|
||||
sizes[album.large ? 'large' : 'small'][breakpoint]
|
||||
)}
|
||||
key={cover}
|
||||
key={album.id}
|
||||
alt='Album Cover'
|
||||
placeholder={null}
|
||||
className={cx(
|
||||
'aspect-square h-full w-full rounded-24',
|
||||
bigCover.includes(index) && 'col-span-2 row-span-2'
|
||||
album.large && 'col-span-2 row-span-2'
|
||||
)}
|
||||
onClick={() => navigate(`/album/${album.id}`)}
|
||||
onMouseOver={() => prefetchAlbum({ id: album.id })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
43
packages/web/components/New/ErrorBoundary.tsx
Normal file
43
packages/web/components/New/ErrorBoundary.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { ReactNode } from 'react'
|
||||
import { ErrorBoundary as ErrorBoundaryRaw } from 'react-error-boundary'
|
||||
|
||||
function ErrorFallback({
|
||||
error,
|
||||
resetErrorBoundary,
|
||||
}: {
|
||||
error: Error
|
||||
resetErrorBoundary: () => void
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
role='alert'
|
||||
className='app-region-drag flex h-screen w-screen items-center justify-center bg-white dark:bg-black'
|
||||
>
|
||||
<div className='app-region-no-drag'>
|
||||
<p>Something went wrong:</p>
|
||||
<pre>{error.message}</pre>
|
||||
<button
|
||||
onClick={resetErrorBoundary}
|
||||
className='mt-4 rounded-full bg-brand-600 px-6 py-5 text-16 font-medium'
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ErrorBoundary = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<ErrorBoundaryRaw
|
||||
FallbackComponent={ErrorFallback}
|
||||
onReset={() => {
|
||||
// reset the state of your app so the error doesn't happen again
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundaryRaw>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorBoundary
|
||||
|
|
@ -12,6 +12,7 @@ const Image = ({
|
|||
sizes,
|
||||
placeholder = 'blank',
|
||||
onClick,
|
||||
onMouseOver,
|
||||
}: {
|
||||
src?: string
|
||||
srcSet?: string
|
||||
|
|
@ -21,6 +22,7 @@ const Image = ({
|
|||
lazyLoad?: boolean
|
||||
placeholder?: 'artist' | 'album' | 'playlist' | 'podcast' | 'blank' | null
|
||||
onClick?: (e: React.MouseEvent<HTMLImageElement>) => void
|
||||
onMouseOver?: (e: React.MouseEvent<HTMLImageElement>) => void
|
||||
}) => {
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [error, setError] = useState(false)
|
||||
|
|
@ -57,6 +59,7 @@ const Image = ({
|
|||
initial={{ opacity: 0 }}
|
||||
transition={transition}
|
||||
onClick={onClick}
|
||||
onMouseOver={onMouseOver}
|
||||
/>
|
||||
|
||||
{/* Placeholder / Error fallback */}
|
||||
|
|
|
|||
|
|
@ -9,18 +9,19 @@ import { useSnapshot } from 'valtio'
|
|||
|
||||
const Layout = () => {
|
||||
const playerSnapshot = useSnapshot(player)
|
||||
const track = useMemo(() => playerSnapshot.track, [playerSnapshot])
|
||||
const showPlayer = useMemo(() => !!playerSnapshot.track, [playerSnapshot])
|
||||
|
||||
return (
|
||||
<div
|
||||
id='layout'
|
||||
className={cx(
|
||||
'grid h-screen select-none overflow-hidden rounded-24 bg-white dark:bg-black',
|
||||
'relative grid h-screen select-none overflow-hidden bg-white dark:bg-black',
|
||||
window.ipcRenderer && 'rounded-24',
|
||||
css`
|
||||
grid-template-columns: 6.5rem auto 358px;
|
||||
grid-template-rows: 132px auto;
|
||||
`,
|
||||
track
|
||||
showPlayer
|
||||
? css`
|
||||
grid-template-areas:
|
||||
'sidebar main -'
|
||||
|
|
@ -36,7 +37,7 @@ const Layout = () => {
|
|||
<Sidebar />
|
||||
<Topbar />
|
||||
<Main />
|
||||
{track && <Player />}
|
||||
{showPlayer && <Player />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,18 +45,21 @@ const Cover = () => {
|
|||
const duration = 150 // ms
|
||||
|
||||
useEffect(() => {
|
||||
const cover = resizeImage(playerSnapshot.track?.al.picUrl || '', 'lg')
|
||||
const resizedCover = resizeImage(
|
||||
playerSnapshot.track?.al.picUrl || '',
|
||||
'lg'
|
||||
)
|
||||
const animate = async () => {
|
||||
animationStartTime.current = Date.now()
|
||||
await controls.start({ opacity: 0 })
|
||||
setCover(cover)
|
||||
setCover(resizedCover)
|
||||
}
|
||||
animate()
|
||||
}, [controls, playerSnapshot.track?.al.picUrl])
|
||||
|
||||
// 防止狂点下一首或上一首造成封面与歌曲不匹配的问题
|
||||
useEffect(() => {
|
||||
const realCover = playerSnapshot.track?.al.picUrl ?? ''
|
||||
const realCover = resizeImage(playerSnapshot.track?.al.picUrl ?? '', 'lg')
|
||||
if (cover !== realCover) setCover(realCover)
|
||||
}, [cover, playerSnapshot.track?.al.picUrl])
|
||||
|
||||
|
|
@ -156,7 +159,7 @@ const NowPlaying = () => {
|
|||
|
||||
<button>
|
||||
<Icon
|
||||
name='repeat-1'
|
||||
name='heart'
|
||||
className='h-7 w-7 text-black/90 dark:text-white/40'
|
||||
/>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,15 @@
|
|||
import { resizeImage } from '@/web/utils/common'
|
||||
import React, { useMemo } from 'react'
|
||||
import { player } from '@/web/store'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import useTracks from '@/web/api/hooks/useTracks'
|
||||
import { css, cx } from '@emotion/css'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import Image from './Image'
|
||||
import Wave from './Wave'
|
||||
|
||||
const PlayingNext = ({ className }: { className?: string }) => {
|
||||
const playerSnapshot = useSnapshot(player)
|
||||
const list = useMemo(
|
||||
() => playerSnapshot.trackList.slice(playerSnapshot.trackIndex + 1, 100),
|
||||
[playerSnapshot.trackList, playerSnapshot.trackIndex]
|
||||
)
|
||||
const { data: tracks } = useTracks({ ids: list })
|
||||
const { data: tracks } = useTracks({ ids: playerSnapshot.trackList })
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -28,19 +24,13 @@ const PlayingNext = ({ className }: { className?: string }) => {
|
|||
className={cx(
|
||||
'relative z-10 overflow-scroll',
|
||||
className,
|
||||
css``,
|
||||
css`
|
||||
padding-top: 42px;
|
||||
mask-image: linear-gradient(to bottom, transparent 0, black 42px);
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
padding-top: 42px;
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
black 42px
|
||||
);
|
||||
mask-image: linear-gradient(to bottom, transparent 0, black 42px);
|
||||
`
|
||||
)}
|
||||
>
|
||||
|
|
@ -57,13 +47,19 @@ const PlayingNext = ({ className }: { className?: string }) => {
|
|||
duration: 0.24,
|
||||
}}
|
||||
layout
|
||||
onClick={e => {
|
||||
if (e.detail === 2) player.playTrack(track.id)
|
||||
}}
|
||||
>
|
||||
{/* Cover */}
|
||||
<Image
|
||||
alt='Cover'
|
||||
className='mr-4 aspect-square h-14 w-14 flex-shrink-0 rounded-12'
|
||||
src={resizeImage(track.al.picUrl, 'sm')}
|
||||
/>
|
||||
<div className='flex-grow'>
|
||||
|
||||
{/* Track info */}
|
||||
<div className='mr-3 flex-grow'>
|
||||
<div className='line-clamp-1 text-18 font-medium text-neutral-700 dark:text-neutral-200'>
|
||||
{track.name}
|
||||
</div>
|
||||
|
|
@ -71,13 +67,21 @@ const PlayingNext = ({ className }: { className?: string }) => {
|
|||
{track.ar.map(a => a.name).join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-18 font-medium text-neutral-700 dark:text-neutral-200'>
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</div>
|
||||
|
||||
{/* Wave icon */}
|
||||
{playerSnapshot.trackIndex === index ? (
|
||||
<Wave playing={playerSnapshot.state === 'playing'} />
|
||||
) : (
|
||||
<div className='text-18 font-medium text-neutral-700 dark:text-neutral-200'>
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
<div className='pointer-events-none sticky bottom-0 h-8 w-full bg-gradient-to-t from-black'></div>
|
||||
{(tracks?.songs.length || 0) >= 4 && (
|
||||
<div className='pointer-events-none sticky bottom-0 h-8 w-full bg-gradient-to-t from-black'></div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Route, RouteObject, Routes, useLocation } from 'react-router-dom'
|
||||
import Login from '@/web/pages/Login'
|
||||
import Playlist from '@/web/pages/Playlist'
|
||||
import Artist from '@/web/pages/Artist'
|
||||
import Search from '@/web/pages/Search'
|
||||
import Library from '@/web/pages/Library'
|
||||
|
|
@ -11,6 +10,7 @@ import React, { ReactNode, Suspense } from 'react'
|
|||
const My = React.lazy(() => import('@/web/pages/New/My'))
|
||||
const Discover = React.lazy(() => import('@/web/pages/New/Discover'))
|
||||
const Album = React.lazy(() => import('@/web/pages/New/Album'))
|
||||
const Playlist = React.lazy(() => import('@/web/pages/New/Playlist'))
|
||||
|
||||
const routes: RouteObject[] = [
|
||||
{
|
||||
|
|
@ -71,6 +71,7 @@ const Router = () => {
|
|||
<Route path='/discover' element={lazy(<Discover />)} />
|
||||
<Route path='/login' element={lazy(<Login />)} />
|
||||
<Route path='/album/:id' element={lazy(<Album />)} />
|
||||
<Route path='/playlist/:id' element={lazy(<Playlist />)} />
|
||||
</Routes>
|
||||
</AnimatePresence>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ const Topbar = () => {
|
|||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'app-region-drag fixed top-0 right-0 z-10 flex items-center justify-between pt-11 pb-10 pr-6 pl-10 ',
|
||||
'app-region-drag fixed top-0 right-0 z-20 flex items-center justify-between overflow-hidden rounded-tr-24 pt-11 pb-10 pr-6 pl-10 ',
|
||||
css`
|
||||
left: 104px;
|
||||
`,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { css, cx } from '@emotion/css'
|
|||
import { useMemo } from 'react'
|
||||
import { player } from '@/web/store'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import Wave from './Wave'
|
||||
|
||||
const TrackList = ({
|
||||
tracks,
|
||||
|
|
@ -23,18 +24,18 @@ const TrackList = ({
|
|||
if (e.detail === 2) onPlay?.(trackID)
|
||||
}
|
||||
|
||||
const playing = useMemo(
|
||||
() => playerSnapshot.state === 'playing',
|
||||
[playerSnapshot.state]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{tracks?.map(track => (
|
||||
<div
|
||||
key={track.id}
|
||||
onClick={e => handleClick(e, track.id)}
|
||||
className={cx(
|
||||
'flex py-2 text-18 font-medium transition duration-300 ease-in-out',
|
||||
playingTrack?.id === track.id
|
||||
? 'text-brand-700'
|
||||
: 'text-night-50 dark:hover:text-neutral-200'
|
||||
)}
|
||||
className='relative flex items-center py-2 text-18 font-medium text-night-50 transition duration-300 ease-in-out dark:hover:text-neutral-200'
|
||||
>
|
||||
<div className='mr-6'>{String(track.no).padStart(2, '0')}</div>
|
||||
<div className='flex-grow'>{track.name}</div>
|
||||
|
|
@ -42,6 +43,13 @@ const TrackList = ({
|
|||
<div className='text-right'>
|
||||
{formatDuration(track.dt, 'en', 'hh:mm:ss')}
|
||||
</div>
|
||||
|
||||
{/* The wave icon */}
|
||||
{playingTrack?.id === track.id && playing && (
|
||||
<div className='absolute -left-7'>
|
||||
<Wave playing={playing} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { formatDuration, resizeImage } from '@/web/utils/common'
|
||||
import { formatDate, formatDuration, resizeImage } from '@/web/utils/common'
|
||||
import { css, cx } from '@emotion/css'
|
||||
import Icon from '@/web/components/Icon'
|
||||
import dayjs from 'dayjs'
|
||||
|
|
@ -7,9 +7,11 @@ import Image from './Image'
|
|||
|
||||
const TrackListHeader = ({
|
||||
album,
|
||||
playlist,
|
||||
onPlay,
|
||||
}: {
|
||||
album?: Album
|
||||
playlist?: Playlist
|
||||
onPlay: () => void
|
||||
}) => {
|
||||
const albumDuration = useMemo(() => {
|
||||
|
|
@ -17,6 +19,8 @@ const TrackListHeader = ({
|
|||
return formatDuration(duration, 'en', 'hh[hr] mm[min]')
|
||||
}, [album?.songs])
|
||||
|
||||
const cover = album?.picUrl || playlist?.coverImgUrl || ''
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
|
|
@ -28,48 +32,58 @@ const TrackListHeader = ({
|
|||
>
|
||||
<Image
|
||||
className='z-10 aspect-square w-full rounded-24'
|
||||
src={resizeImage(album?.picUrl || '', 'lg')}
|
||||
src={resizeImage(cover, 'lg')}
|
||||
alt='Cover'
|
||||
/>
|
||||
|
||||
{/* Blur bg */}
|
||||
<img
|
||||
className={cx(
|
||||
'fixed z-0 object-cover opacity-70',
|
||||
'absolute z-0 object-cover opacity-70',
|
||||
css`
|
||||
top: -400px;
|
||||
left: -370px;
|
||||
width: 1572px;
|
||||
height: 528px;
|
||||
filter: blur(256px) saturate(1.2);
|
||||
/* transform: scale(0.5); */
|
||||
`
|
||||
)}
|
||||
src={resizeImage(album?.picUrl || '', 'lg')}
|
||||
src={resizeImage(cover, 'sm')}
|
||||
/>
|
||||
|
||||
<div className=' flex flex-col justify-between'>
|
||||
<div className='flex flex-col justify-between'>
|
||||
<div>
|
||||
<div className='text-36 font-medium dark:text-neutral-100'>
|
||||
{album?.name}
|
||||
{album?.name || playlist?.name}
|
||||
</div>
|
||||
<div className='mt-6 text-24 font-medium dark:text-neutral-600'>
|
||||
{album?.artist.name}
|
||||
{album?.artist.name || playlist?.creator.nickname}
|
||||
</div>
|
||||
<div className='mt-1 flex items-center text-14 font-bold dark:text-neutral-600'>
|
||||
{album?.mark === 1056768 && (
|
||||
<Icon name='explicit' className='mb-px mr-1 h-3.5 w-3.5 ' />
|
||||
)}{' '}
|
||||
{dayjs(album?.publishTime || 0).year()} · {album?.songs.length}{' '}
|
||||
Songs, {albumDuration}
|
||||
{!!album && (
|
||||
<>
|
||||
{album?.mark === 1056768 && (
|
||||
<Icon name='explicit' className='mb-px mr-1 h-3.5 w-3.5 ' />
|
||||
)}{' '}
|
||||
{dayjs(album?.publishTime || 0).year()} · {album?.songs.length}{' '}
|
||||
Tracks, {albumDuration}
|
||||
</>
|
||||
)}
|
||||
{!!playlist && (
|
||||
<>
|
||||
Updated at {formatDate(playlist?.updateTime || 0, 'en')} ·{' '}
|
||||
{playlist.trackCount} Tracks
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className='line-clamp-3 mt-6 text-14 font-bold dark:text-neutral-600'>
|
||||
{album?.description}
|
||||
<div className='line-clamp-3 mt-6 whitespace-pre-wrap text-14 font-bold dark:text-neutral-600'>
|
||||
{album?.description || playlist?.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='z-10 flex'>
|
||||
<button
|
||||
onClick={onPlay}
|
||||
onClick={() => onPlay()}
|
||||
className='h-[72px] w-[170px] rounded-full dark:bg-brand-700'
|
||||
></button>
|
||||
<button className='ml-6 h-[72px] w-[72px] rounded-full dark:bg-night-50'></button>
|
||||
|
|
|
|||
32
packages/web/components/New/Wave.tsx
Normal file
32
packages/web/components/New/Wave.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { css, cx, keyframes } from '@emotion/css'
|
||||
|
||||
const Wave = ({ playing }: { playing: boolean }) => {
|
||||
const wave = keyframes`
|
||||
0% { transform: scaleY(1) }
|
||||
50% { transform: scaleY(0.2) }
|
||||
100% { transform: scaleY(1)}
|
||||
`
|
||||
const animation = css`
|
||||
transform-origin: bottom;
|
||||
animation: ${wave} 1s ease-in-out infinite;
|
||||
`
|
||||
|
||||
const delay = ['-100ms', '-500ms', '-1200ms', '-1000ms', '-700ms']
|
||||
|
||||
return (
|
||||
<div className='grid h-3 grid-cols-5 items-end gap-0.5'>
|
||||
{[...new Array(5).keys()].map(i => (
|
||||
<div
|
||||
key={i}
|
||||
className={cx('h-full w-0.5 bg-brand-600', animation)}
|
||||
style={{
|
||||
animationDelay: delay[i],
|
||||
animationPlayState: playing ? 'running' : 'paused',
|
||||
}}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Wave
|
||||
Loading…
Add table
Add a link
Reference in a new issue