YesPlayMusic/packages/web/components/Player.tsx

245 lines
7.3 KiB
TypeScript
Raw Normal View History

2022-04-09 00:28:37 +08:00
import ArtistInline from './ArtistsInline'
import IconButton from './IconButton'
import Slider from './Slider'
import SvgIcon from './SvgIcon'
2022-04-05 02:30:07 +08:00
import useUserLikedTracksIDs, {
useMutationLikeATrack,
2022-05-12 02:45:43 +08:00
} from '@/web/hooks/useUserLikedTracksIDs'
import { player, state } from '@/web/store'
import { resizeImage } from '@/web/utils/common'
2022-04-09 00:28:37 +08:00
import {
State as PlayerState,
Mode as PlayerMode,
2022-05-12 02:45:43 +08:00
} from '@/web/utils/player'
import { RepeatMode as PlayerRepeatMode } from '@/shared/playerDataTypes'
2022-05-12 02:45:43 +08:00
import cx from 'classnames'
import { useSnapshot } from 'valtio'
import { useMemo } from 'react'
import toast from 'react-hot-toast'
import { useNavigate } from 'react-router-dom'
2022-03-13 14:40:38 +08:00
const PlayingTrack = () => {
const navigate = useNavigate()
const snappedPlayer = useSnapshot(player)
const track = useMemo(() => snappedPlayer.track, [snappedPlayer.track])
const trackListSource = useMemo(
() => snappedPlayer.trackListSource,
[snappedPlayer.trackListSource]
)
// Liked songs ids
2022-04-05 02:30:07 +08:00
const { data: userLikedSongs } = useUserLikedTracksIDs()
const mutationLikeATrack = useMutationLikeATrack()
const hasTrackListSource =
snappedPlayer.mode !== PlayerMode.FM && trackListSource?.type
2022-03-13 14:40:38 +08:00
const toAlbum = () => {
const id = track?.al?.id
if (id) navigate(`/album/${id}`)
}
const toTrackListSource = () => {
if (hasTrackListSource)
2022-03-13 14:40:38 +08:00
navigate(`/${trackListSource.type}/${trackListSource.id}`)
}
return (
2022-03-29 00:11:05 +08:00
<>
2022-03-13 14:40:38 +08:00
{track && (
2022-03-17 19:30:43 +08:00
<div className='flex items-center gap-3'>
2022-03-13 14:40:38 +08:00
{track?.al?.picUrl && (
<img
onClick={toAlbum}
2022-03-17 19:30:43 +08:00
className='aspect-square h-full rounded-md shadow-md'
2022-04-02 18:46:08 +08:00
src={resizeImage(track.al.picUrl, 'xs')}
2022-03-13 14:40:38 +08:00
/>
)}
{!track?.al?.picUrl && (
<div
onClick={toAlbum}
2022-03-17 19:30:43 +08:00
className='flex aspect-square h-full items-center justify-center rounded-md bg-black/[.04] shadow-sm'
2022-03-13 14:40:38 +08:00
>
2022-03-17 19:30:43 +08:00
<SvgIcon className='h-6 w-6 text-gray-300' name='music-note' />
2022-03-13 14:40:38 +08:00
</div>
)}
2022-03-17 19:30:43 +08:00
<div className='flex flex-col justify-center leading-tight'>
2022-03-13 14:40:38 +08:00
<div
onClick={toTrackListSource}
2022-05-12 02:45:43 +08:00
className={cx(
'line-clamp-1 font-semibold text-black decoration-gray-600 decoration-2 dark:text-white dark:decoration-gray-300',
hasTrackListSource && 'hover:underline'
)}
2022-03-13 14:40:38 +08:00
>
{track?.name}
</div>
2022-03-17 19:30:43 +08:00
<div className='mt-0.5 text-xs text-gray-500 dark:text-gray-400'>
2022-03-13 14:40:38 +08:00
<ArtistInline artists={track?.ar ?? []} />
</div>
</div>
2022-04-05 02:30:07 +08:00
<IconButton
onClick={() => track?.id && mutationLikeATrack.mutate(track.id)}
>
2022-03-13 14:40:38 +08:00
<SvgIcon
2022-04-04 17:51:07 +08:00
className='h-5 w-5 text-black dark:text-white'
name={
track?.id && userLikedSongs?.ids?.includes(track.id)
? 'heart'
: 'heart-outline'
}
2022-03-13 14:40:38 +08:00
/>
</IconButton>
</div>
)}
{!track && <div></div>}
2022-03-29 00:11:05 +08:00
</>
2022-03-13 14:40:38 +08:00
)
}
const MediaControls = () => {
const playerSnapshot = useSnapshot(player)
const state = useMemo(() => playerSnapshot.state, [playerSnapshot.state])
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
const mode = useMemo(() => playerSnapshot.mode, [playerSnapshot.mode])
2022-04-02 16:54:37 +08:00
2022-03-13 14:40:38 +08:00
return (
2022-03-17 19:30:43 +08:00
<div className='flex items-center justify-center gap-2 text-black dark:text-white'>
{mode === PlayerMode.TrackList && (
<IconButton
onClick={() => track && player.prevTrack()}
disabled={!track}
>
<SvgIcon className='h-6 w-6' name='previous' />
</IconButton>
)}
{mode === PlayerMode.FM && (
<IconButton onClick={() => player.fmTrash()}>
<SvgIcon className='h-6 w-6' name='dislike' />
</IconButton>
)}
2022-03-13 14:40:38 +08:00
<IconButton
onClick={() => track && player.playOrPause()}
disabled={!track}
2022-03-21 02:03:25 +08:00
className='after:rounded-xl'
2022-03-13 14:40:38 +08:00
>
<SvgIcon
2022-03-21 02:03:25 +08:00
className='h-7 w-7'
2022-03-19 17:03:29 +08:00
name={
[PlayerState.Playing, PlayerState.Loading].includes(state)
2022-03-19 17:03:29 +08:00
? 'pause'
: 'play'
}
2022-03-13 14:40:38 +08:00
/>
</IconButton>
<IconButton onClick={() => track && player.nextTrack()} disabled={!track}>
2022-03-21 02:03:25 +08:00
<SvgIcon className='h-6 w-6' name='next' />
2022-03-13 14:40:38 +08:00
</IconButton>
</div>
)
}
const Others = () => {
const playerSnapshot = useSnapshot(player)
const switchRepeatMode = () => {
if (playerSnapshot.repeatMode === PlayerRepeatMode.Off) {
player.repeatMode = PlayerRepeatMode.On
} else if (playerSnapshot.repeatMode === PlayerRepeatMode.On) {
player.repeatMode = PlayerRepeatMode.One
} else {
player.repeatMode = PlayerRepeatMode.Off
}
}
2022-03-13 14:40:38 +08:00
return (
2022-03-17 19:30:43 +08:00
<div className='flex items-center justify-end gap-2 pr-2 text-black dark:text-white'>
2022-04-02 02:12:15 +08:00
<IconButton
onClick={() => toast('Work in progress')}
disabled={playerSnapshot.mode === PlayerMode.FM}
2022-04-02 02:12:15 +08:00
>
2022-03-21 02:03:25 +08:00
<SvgIcon className='h-6 w-6' name='playlist' />
2022-03-13 14:40:38 +08:00
</IconButton>
2022-04-02 02:12:15 +08:00
<IconButton
onClick={switchRepeatMode}
disabled={playerSnapshot.mode === PlayerMode.FM}
2022-04-02 02:12:15 +08:00
>
<SvgIcon
2022-05-12 02:45:43 +08:00
className={cx(
'h-6 w-6',
playerSnapshot.repeatMode !== PlayerRepeatMode.Off &&
'text-brand-500'
)}
name={
playerSnapshot.repeatMode === PlayerRepeatMode.One
? 'repeat-1'
: 'repeat'
}
/>
2022-03-13 14:40:38 +08:00
</IconButton>
2022-04-02 02:12:15 +08:00
<IconButton
onClick={() => toast('施工中...')}
disabled={playerSnapshot.mode === PlayerMode.FM}
2022-04-02 02:12:15 +08:00
>
2022-03-21 02:03:25 +08:00
<SvgIcon className='h-6 w-6' name='shuffle' />
2022-03-13 14:40:38 +08:00
</IconButton>
2022-04-02 02:12:15 +08:00
<IconButton onClick={() => toast('施工中...')}>
2022-03-21 02:03:25 +08:00
<SvgIcon className='h-6 w-6' name='volume' />
2022-03-13 14:40:38 +08:00
</IconButton>
2022-04-08 01:02:25 +08:00
{/* Lyric */}
<IconButton onClick={() => (state.uiStates.showLyricPanel = true)}>
2022-03-21 02:03:25 +08:00
<SvgIcon className='h-6 w-6' name='lyrics' />
2022-03-13 14:40:38 +08:00
</IconButton>
</div>
)
}
const Progress = () => {
const playerSnapshot = useSnapshot(player)
const progress = useMemo(
() => playerSnapshot.progress,
[playerSnapshot.progress]
)
2022-03-19 17:03:29 +08:00
const state = useMemo(() => playerSnapshot.state, [playerSnapshot.state])
2022-03-13 14:40:38 +08:00
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
return (
2022-03-17 19:30:43 +08:00
<div className='absolute w-screen'>
2022-03-13 14:40:38 +08:00
{track && (
<Slider
min={0}
max={(track.dt ?? 0) / 1000}
2022-03-19 17:03:29 +08:00
value={
state === PlayerState.Playing || state === PlayerState.Paused
2022-03-19 17:03:29 +08:00
? progress
: 0
}
2022-03-13 14:40:38 +08:00
onChange={value => {
player.progress = value
}}
onlyCallOnChangeAfterDragEnded={true}
/>
)}
{!track && (
2022-03-17 19:30:43 +08:00
<div className='absolute h-[2px] w-full bg-gray-500 bg-opacity-10'></div>
2022-03-13 14:40:38 +08:00
)}
</div>
)
}
const Player = () => {
return (
2022-03-17 19:30:43 +08:00
<div className='fixed bottom-0 left-0 right-0 grid h-16 grid-cols-3 grid-rows-1 bg-white bg-opacity-[.86] py-2.5 px-5 backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222] dark:bg-opacity-[.86]'>
2022-03-13 14:40:38 +08:00
<Progress />
<PlayingTrack />
<MediaControls />
<Others />
</div>
)
}
export default Player