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,114 @@
import persistedUiStates from '@/web/states/persistedUiStates'
import player from '@/web/states/player'
import { ease } from '@/web/utils/const'
import { cx, css } from '@emotion/css'
import { MotionConfig, motion } from 'framer-motion'
import { useSnapshot } from 'valtio'
import Icon from '../Icon'
import { State as PlayerState } from '@/web/utils/player'
import useUserLikedTracksIDs, {
useMutationLikeATrack,
} from '@/web/api/hooks/useUserLikedTracksIDs'
const LikeButton = () => {
const { track } = useSnapshot(player)
const { data: likedIDs } = useUserLikedTracksIDs()
const isLiked = !!likedIDs?.ids?.find(id => id === track?.id)
const likeATrack = useMutationLikeATrack()
const { minimizePlayer: mini } = useSnapshot(persistedUiStates)
return (
<motion.button
layout='position'
animate={{ rotate: mini ? 90 : 0 }}
onClick={() => track?.id && likeATrack.mutateAsync(track.id)}
className='text-black/90 transition-colors duration-400 dark:text-white/40 hover:dark:text-white/90'
>
<Icon name={isLiked ? 'heart' : 'heart-outline'} className='h-7 w-7' />
</motion.button>
)
}
const Controls = () => {
const { state, track } = useSnapshot(player)
const { minimizePlayer: mini } = useSnapshot(persistedUiStates)
return (
<MotionConfig transition={{ ease, duration: 0.6 }}>
<motion.div
className={cx(
'fixed bottom-0 right-0 flex',
mini
? 'flex-col items-center justify-between'
: 'items-center justify-between',
mini
? css`
right: 24px;
bottom: 18px;
width: 44px;
height: 254px;
`
: css`
bottom: 56px;
right: 56px;
width: 254px;
`
)}
>
{/* Minimize */}
<motion.button
layout='position'
animate={{ rotate: mini ? 90 : 0 }}
className='text-black/90 transition-colors duration-400 dark:text-white/40 hover:dark:text-white/90'
onClick={() => {
persistedUiStates.minimizePlayer = !mini
}}
>
<Icon name='hide-list' className='h-7 w-7 ' />
</motion.button>
{/* Media controls */}
<div className='flex flex-wrap gap-2 text-black/95 dark:text-white/80'>
<motion.button
layout='position'
animate={{ rotate: mini ? 90 : 0 }}
onClick={() => track && player.prevTrack()}
disabled={!track}
className='rounded-full bg-black/10 p-2.5 transition-colors duration-400 dark:bg-white/10 hover:dark:bg-white/20'
>
<Icon name='previous' className='h-6 w-6' />
</motion.button>
<motion.button
layout='position'
animate={{ rotate: mini ? 90 : 0 }}
onClick={() => track && player.playOrPause()}
className='rounded-full bg-black/10 p-2.5 transition-colors duration-400 dark:bg-white/10 hover:dark:bg-white/20'
>
<Icon
name={
[PlayerState.Playing, PlayerState.Loading].includes(state)
? 'pause'
: 'play'
}
className='h-6 w-6 '
/>
</motion.button>
<motion.button
layout='position'
animate={{ rotate: mini ? 90 : 0 }}
onClick={() => track && player.nextTrack()}
disabled={!track}
className='rounded-full bg-black/10 p-2.5 transition-colors duration-400 dark:bg-white/10 hover:dark:bg-white/20'
>
<Icon name='next' className='h-6 w-6 ' />
</motion.button>
</div>
{/* Like */}
<LikeButton />
</motion.div>
</MotionConfig>
)
}
export default Controls

View file

@ -0,0 +1,59 @@
import player from '@/web/states/player'
import { resizeImage } from '@/web/utils/common'
import { ease } from '@/web/utils/const'
import { cx } from '@emotion/css'
import { useAnimation, motion } from 'framer-motion'
import { useState, useRef, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useSnapshot } from 'valtio'
const Cover = () => {
const { track } = useSnapshot(player)
const [cover, setCover] = useState(track?.al.picUrl)
const animationStartTime = useRef(0)
const controls = useAnimation()
const duration = 150 // ms
const navigate = useNavigate()
useEffect(() => {
const resizedCover = resizeImage(track?.al.picUrl || '', 'lg')
const animate = async () => {
animationStartTime.current = Date.now()
await controls.start({ opacity: 0 })
setCover(resizedCover)
}
animate()
}, [controls, track?.al.picUrl])
// 防止狂点下一首或上一首造成封面与歌曲不匹配的问题
useEffect(() => {
const realCover = resizeImage(track?.al.picUrl ?? '', 'lg')
if (cover !== realCover) setCover(realCover)
}, [cover, track?.al.picUrl])
const onLoad = () => {
const passedTime = Date.now() - animationStartTime.current
controls.start({
opacity: 1,
transition: {
delay: passedTime > duration ? 0 : (duration - passedTime) / 1000,
},
})
}
return (
<motion.img
animate={controls}
transition={{ duration: duration / 1000, ease }}
className={cx('absolute inset-0 w-full')}
src={cover}
onLoad={onLoad}
onClick={() => {
const id = track?.al.id
if (id) navigate(`/album/${id}`)
}}
/>
)
}
export default Cover

View file

@ -0,0 +1,23 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import NowPlaying from './NowPlaying'
import tracks from '@/web/.storybook/mock/tracks'
import { sample } from 'lodash-es'
export default {
title: 'Components/NowPlaying',
component: NowPlaying,
parameters: {
viewport: {
defaultViewport: 'iphone8p',
},
},
} as ComponentMeta<typeof NowPlaying>
const Template: ComponentStory<typeof NowPlaying> = args => (
<div className='fixed inset-0 bg-[#F8F8F8] p-4 dark:bg-black'>
<NowPlaying track={sample(tracks)} />
</div>
)
export const Default = Template.bind({})

View file

@ -0,0 +1,65 @@
import { css, cx } from '@emotion/css'
import player from '@/web/states/player'
import { useSnapshot } from 'valtio'
import { AnimatePresence, motion } from 'framer-motion'
import ArtistInline from '@/web/components/ArtistsInline'
import persistedUiStates from '@/web/states/persistedUiStates'
import Controls from './Controls'
import Cover from './Cover'
import Progress from './Progress'
const NowPlaying = () => {
const { track } = useSnapshot(player)
const { minimizePlayer } = useSnapshot(persistedUiStates)
return (
<>
{/* Now Playing */}
<AnimatePresence>
{!minimizePlayer && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={cx(
'relative flex aspect-square h-full w-full flex-col justify-end overflow-hidden rounded-24 border',
css`
border-color: hsl(0, 100%, 100%, 0.08);
`
)}
>
{/* Cover */}
<Cover />
{/* Info & Controls */}
<div className='m-3 flex flex-col items-center rounded-20 bg-white/60 p-5 font-medium backdrop-blur-3xl dark:bg-black/70'>
{/* Track Info */}
<div className='line-clamp-1 text-lg text-black dark:text-white'>
{track?.name}
</div>
<ArtistInline
artists={track?.ar || []}
className='text-black/30 dark:text-white/30'
hoverClassName='hover:text-black/50 dark:hover:text-white/70 transition-colors duration-400'
/>
{/* Dividing line */}
<div className='mt-2 h-px w-2/5 bg-black/10 dark:bg-white/10'></div>
{/* Progress */}
<Progress />
{/* Controls placeholder */}
<div className='h-11'></div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Controls */}
<Controls />
</>
)
}
export default NowPlaying

View file

@ -0,0 +1,29 @@
import player from '@/web/states/player'
import { formatDuration } from '@/web/utils/common'
import { useSnapshot } from 'valtio'
import Slider from '../Slider'
const Progress = () => {
const { track, progress } = useSnapshot(player)
return (
<div className='mt-9 mb-4 flex w-full flex-col'>
<Slider
min={0}
max={(track?.dt ?? 100000) / 1000}
value={progress}
onChange={value => {
player.progress = value
}}
onlyCallOnChangeAfterDragEnded={true}
/>
<div className='mt-1 flex justify-between text-14 font-bold text-black/20 dark:text-white/20'>
<span>{formatDuration(progress * 1000, 'en', 'hh:mm:ss')}</span>
<span>{formatDuration(track?.dt || 0, 'en', 'hh:mm:ss')}</span>
</div>
</div>
)
}
export default Progress

View file

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