feat: updates

This commit is contained in:
qier222 2022-06-12 15:29:14 +08:00
parent 196a974a64
commit 8f4c3d8e5b
No known key found for this signature in database
GPG key ID: 9C85007ED905F14D
24 changed files with 572 additions and 93 deletions

View file

@ -22,7 +22,7 @@ const Image = ({
className?: string
alt: string
lazyLoad?: boolean
placeholder?: 'artist' | 'album' | 'playlist' | 'podcast' | 'blank' | null
placeholder?: 'artist' | 'album' | 'playlist' | 'podcast' | 'blank' | false
onClick?: (e: React.MouseEvent<HTMLImageElement>) => void
onMouseOver?: (e: React.MouseEvent<HTMLImageElement>) => void
animation?: boolean
@ -48,6 +48,7 @@ const Image = ({
? {
animate,
initial: { opacity: 0 },
exit: { opacity: 0 },
transition,
}
: {}
@ -60,22 +61,30 @@ const Image = ({
: {}
return (
<div className={cx('relative overflow-hidden', className)}>
<div
className={cx(
'overflow-hidden',
className,
className?.includes('absolute') === false && 'relative'
)}
>
{/* Image */}
<motion.img
alt={alt}
className='absolute inset-0 h-full w-full'
src={src}
srcSet={srcSet}
sizes={sizes}
decoding='async'
loading={lazyLoad ? 'lazy' : undefined}
onError={onError}
onLoad={onLoad}
onClick={onClick}
onMouseOver={onMouseOver}
{...motionProps}
/>
<AnimatePresence>
<motion.img
alt={alt}
className='absolute inset-0 h-full w-full'
src={src}
srcSet={srcSet}
sizes={sizes}
decoding='async'
loading={lazyLoad ? 'lazy' : undefined}
onError={onError}
onLoad={onLoad}
onClick={onClick}
onMouseOver={onMouseOver}
{...motionProps}
/>
</AnimatePresence>
{/* Placeholder / Error fallback */}
<AnimatePresence>

View file

@ -4,6 +4,7 @@ import Router from './Router'
const Main = () => {
return (
<main
id='main'
className={cx(
'no-scrollbar overflow-y-auto pb-16 pr-6 pl-10',
css`

View file

@ -8,6 +8,10 @@ import { AnimatePresence, motion } from 'framer-motion'
import Image from './Image'
import Wave from './Wave'
import Icon from '@/web/components/Icon'
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef } from 'react'
import { useWindowSize } from 'react-use'
import { playerWidth, topbarHeight } from '@/web/utils/const'
const Header = () => {
return (
@ -46,13 +50,13 @@ const Track = ({
return (
<motion.div
className='flex items-center justify-between'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ x: '100%', opacity: 0 }}
transition={{
duration: 0.24,
}}
layout
// initial={{ opacity: 0 }}
// animate={{ opacity: 1 }}
// exit={{ x: '100%', opacity: 0 }}
// transition={{
// duration: 0.24,
// }}
// layout
onClick={e => {
if (e.detail === 2 && track?.id) player.playTrack(track.id)
}}
@ -62,6 +66,8 @@ const Track = ({
alt='Cover'
className='mr-4 aspect-square h-14 w-14 flex-shrink-0 rounded-12'
src={resizeImage(track?.al?.picUrl || '', 'sm')}
animation={false}
placeholder={false}
/>
{/* Track info */}
@ -93,41 +99,82 @@ const Track = ({
)
}
const PlayingNext = ({ className }: { className?: string }) => {
const TrackList = ({ className }: { className?: string }) => {
const { trackList, trackIndex, state } = useSnapshot(player)
const { data: tracks } = useTracks({ ids: trackList })
const { data: tracksRaw } = useTracks({ ids: trackList })
const tracks = tracksRaw?.songs || []
const parentRef = useRef<HTMLDivElement>(null)
const { height } = useWindowSize()
const listHeight = height - topbarHeight - playerWidth - 24 - 20 // 24是封面与底部间距20是list与封面间距
const rowVirtualizer = useVirtualizer({
count: tracks.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 76,
overscan: 5,
})
return (
<>
<Header />
<div
ref={parentRef}
style={{
height: `${listHeight}px`,
}}
className={cx(
'no-scrollbar relative z-10 overflow-scroll',
'no-scrollbar relative z-10 w-full overflow-auto',
className,
css`
padding-top: 42px;
mask-image: linear-gradient(to bottom, transparent 0, black 42px);
mask-image: linear-gradient(
to bottom,
transparent 0,
black 42px
); // 顶部渐变遮罩
`
)}
>
<motion.div className='grid gap-4'>
<AnimatePresence>
{tracks?.songs?.map((track, index) => (
<div
className='relative w-full'
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
}}
>
{rowVirtualizer.getVirtualItems().map((row: any) => (
<div
key={row.index}
className='absolute top-0 left-0 w-full'
style={{
height: `${row.size}px`,
transform: `translateY(${row.start}px)`,
}}
>
<Track
key={track.id}
track={track}
index={index}
track={tracks?.[row.index]}
index={row.index}
playingTrackIndex={trackIndex}
state={state}
/>
))}
{(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>
))}
</div>
</div>
{/* 底部渐变遮罩 */}
<div
className='pointer-events-none absolute right-0 left-0 z-20 h-14 bg-gradient-to-t from-black to-transparent'
style={{ top: `${listHeight - 56}px` }}
></div>
</>
)
}
const PlayingNext = ({ className }: { className?: string }) => {
return (
<>
<Header />
<TrackList className={className} />
</>
)
}

View file

@ -1,10 +1,134 @@
import { formatDate, formatDuration, resizeImage } from '@/web/utils/common'
import {
formatDate,
formatDuration,
isIOS,
isSafari,
resizeImage,
} from '@/web/utils/common'
import { css, cx } from '@emotion/css'
import Icon from '@/web/components/Icon'
import dayjs from 'dayjs'
import { useMemo } from 'react'
import Image from './Image'
import useIsMobile from '@/web/hooks/useIsMobile'
import { memo, useEffect, useMemo, useRef } from 'react'
import Hls from 'hls.js'
import Plyr, { APITypes, PlyrProps, PlyrInstance } from 'plyr-react'
import useVideoCover from '@/web/hooks/useVideoCover'
import { motion } from 'framer-motion'
import { ease } from '@/web/utils/const'
import { injectGlobal } from '@emotion/css'
injectGlobal`
.plyr__video-wrapper,
.plyr--video {
background-color: transparent !important;
}
`
const VideoCover = ({ source }: { source?: string }) => {
const ref = useRef<APITypes>(null)
useEffect(() => {
const loadVideo = async () => {
if (!source || !Hls.isSupported()) return
const video = document.getElementById('plyr') as HTMLVideoElement
const hls = new Hls()
hls.loadSource(source)
hls.attachMedia(video)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
ref.current!.plyr.media = video
hls.on(Hls.Events.MANIFEST_PARSED, () => {
// eslint-disable-next-line @typescript-eslint/no-extra-semi
;(ref.current!.plyr as PlyrInstance).play()
})
}
loadVideo()
})
return (
<div className='z-10 aspect-square overflow-hidden rounded-24'>
<Plyr
id='plyr'
options={{
volume: 0,
controls: [],
autoplay: true,
clickToPlay: false,
loop: {
active: true,
},
}}
source={{} as PlyrProps['source']}
ref={ref}
/>
</div>
)
}
const Cover = memo(
({ album, playlist }: { album?: Album; playlist?: Playlist }) => {
const isMobile = useIsMobile()
const { data: videoCover } = useVideoCover({
id: album?.id,
name: album?.name,
artist: album?.artist.name,
})
const cover = album?.picUrl || playlist?.coverImgUrl || ''
return (
<>
<div className='relative z-10 aspect-square w-full overflow-auto rounded-24 '>
<Image
className='absolute inset-0 h-full w-full'
src={resizeImage(cover, 'lg')}
alt='Cover'
/>
{videoCover && (
<motion.div
initial={{ opacity: isIOS ? 1 : 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, ease }}
className='absolute inset-0 h-full w-full'
>
{isSafari ? (
<video
src={videoCover}
className='h-full w-full'
autoPlay
loop
muted
playsInline
></video>
) : (
<VideoCover source={videoCover} />
)}
</motion.div>
)}
</div>
{/* Blur bg */}
{!isMobile && (
<img
className={cx(
'absolute z-0 object-cover opacity-70',
css`
top: -400px;
left: -370px;
width: 1572px;
height: 528px;
filter: blur(256px) saturate(1.2);
`
)}
src={resizeImage(cover, 'sm')}
/>
)}
</>
)
}
)
Cover.displayName = 'Cover'
const TrackListHeader = ({
album,
@ -15,18 +139,16 @@ const TrackListHeader = ({
playlist?: Playlist
onPlay: () => void
}) => {
const isMobile = useIsMobile()
const albumDuration = useMemo(() => {
const duration = album?.songs?.reduce((acc, cur) => acc + cur.dt, 0) || 0
return formatDuration(duration, 'en', 'hh[hr] mm[min]')
}, [album?.songs])
const isMobile = useIsMobile()
const cover = album?.picUrl || playlist?.coverImgUrl || ''
return (
<div
className={cx(
'mx-2.5 rounded-48 p-8 dark:bg-white/10',
'z-10 mx-2.5 rounded-48 p-8 dark:bg-white/10',
'lg:mx-0 lg:grid lg:grid-rows-1 lg:gap-10 lg:rounded-none lg:p-0 lg:dark:bg-transparent',
!isMobile &&
css`
@ -34,29 +156,7 @@ const TrackListHeader = ({
`
)}
>
{/* Cover */}
<Image
className='z-10 aspect-square w-full rounded-24'
src={resizeImage(cover, 'lg')}
alt='Cover'
/>
{/* Blur bg */}
{!isMobile && (
<img
className={cx(
'absolute z-0 object-cover opacity-70',
css`
top: -400px;
left: -370px;
width: 1572px;
height: 528px;
filter: blur(256px) saturate(1.2);
`
)}
src={resizeImage(cover, 'sm')}
/>
)}
<Cover {...{ album, playlist }} />
<div className='flex flex-col justify-between'>
<div>