feat: monorepo

This commit is contained in:
qier222 2022-05-12 02:45:43 +08:00
parent 4d54060a4f
commit 42089d4996
No known key found for this signature in database
GPG key ID: 9C85007ED905F14D
200 changed files with 1530 additions and 1521 deletions

View file

@ -0,0 +1,115 @@
import useLyric from '@/web/hooks/useLyric'
import { player } from '@/web/store'
import { motion } from 'framer-motion'
import { lyricParser } from '@/web/utils/lyric'
import { useMemo } from 'react'
import { useSnapshot } from 'valtio'
import cx from 'classnames'
const Lyric = ({ className }: { className?: string }) => {
// const ease = [0.5, 0.2, 0.2, 0.8]
const playerSnapshot = useSnapshot(player)
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
const { data: lyricRaw } = useLyric({ id: track?.id ?? 0 })
const lyric = useMemo(() => {
return lyricRaw && lyricParser(lyricRaw)
}, [lyricRaw])
const progress = playerSnapshot.progress + 0.3
const currentLine = useMemo(() => {
const index =
(lyric?.lyric.findIndex(({ time }) => time > progress) ?? 1) - 1
return {
index: index < 1 ? 0 : index,
time: lyric?.lyric?.[index]?.time ?? 0,
}
}, [lyric?.lyric, progress])
const displayLines = useMemo(() => {
const index = currentLine.index
const lines =
lyric?.lyric.slice(index === 0 ? 0 : index - 1, currentLine.index + 7) ??
[]
if (index === 0) {
lines.unshift({
time: 0,
content: '',
rawTime: '[00:00:00]',
})
}
return lines
}, [currentLine.index, lyric?.lyric])
const variants = {
initial: { opacity: [0, 0.2], y: ['24%', 0] },
current: {
opacity: 1,
y: 0,
transition: {
ease: [0.5, 0.2, 0.2, 0.8],
duration: 0.7,
},
},
rest: (index: number) => ({
opacity: 0.2,
y: 0,
transition: {
delay: index * 0.04,
ease: [0.5, 0.2, 0.2, 0.8],
duration: 0.7,
},
}),
exit: {
opacity: 0,
y: -132,
height: 0,
paddingTop: 0,
paddingBottom: 0,
transition: {
duration: 0.7,
ease: [0.5, 0.2, 0.2, 0.8],
},
},
}
return (
<div
className={cx(
'max-h-screen cursor-default overflow-hidden font-semibold',
className
)}
style={{
paddingTop: 'calc(100vh / 7 * 3)',
paddingBottom: 'calc(100vh / 7 * 3)',
fontSize: 'calc(100vw * 0.0264)',
lineHeight: 'calc(100vw * 0.032)',
}}
>
{displayLines.map(({ content, time }, index) => {
return (
<motion.div
key={`${String(index)}-${String(time)}`}
custom={index}
variants={variants}
initial={'initial'}
animate={
time === currentLine.time
? 'current'
: time < currentLine.time
? 'exit'
: 'rest'
}
layout
className={cx('max-w-[78%] py-[calc(100vw_*_0.0111)] text-white')}
>
{content}
</motion.div>
)
})}
</div>
)
}
export default Lyric

View file

@ -0,0 +1,101 @@
import useLyric from '@/web/hooks/useLyric'
import { player } from '@/web/store'
import { motion, useMotionValue } from 'framer-motion'
import { lyricParser } from '@/web/utils/lyric'
import { useWindowSize } from 'react-use'
import { useEffect, useLayoutEffect, useMemo, useState } from 'react'
import { useSnapshot } from 'valtio'
import cx from 'classnames'
const Lyric = ({ className }: { className?: string }) => {
// const ease = [0.5, 0.2, 0.2, 0.8]
const playerSnapshot = useSnapshot(player)
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
const { data: lyricRaw } = useLyric({ id: track?.id ?? 0 })
const lyric = useMemo(() => {
return lyricRaw && lyricParser(lyricRaw)
}, [lyricRaw])
const [progress, setProgress] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setProgress(player.howler.seek() + 0.3)
}, 300)
return () => clearInterval(timer)
}, [])
const currentIndex = useMemo(() => {
return (lyric?.lyric.findIndex(({ time }) => time > progress) ?? 1) - 1
}, [lyric?.lyric, progress])
const y = useMotionValue(1000)
const { height: windowHight } = useWindowSize()
useLayoutEffect(() => {
const top = (
document.getElementById('lyrics')?.children?.[currentIndex] as any
)?.offsetTop
if (top) {
y.set((windowHight / 9) * 4 - top)
}
}, [currentIndex, windowHight, y])
useEffect(() => {
y.set(0)
}, [track, y])
return (
<div
className={cx(
'max-h-screen cursor-default select-none overflow-hidden font-semibold',
className
)}
style={{
paddingTop: 'calc(100vh / 9 * 4)',
paddingBottom: 'calc(100vh / 9 * 4)',
fontSize: 'calc(100vw * 0.0264)',
lineHeight: 'calc(100vw * 0.032)',
}}
id='lyrics'
>
{lyric?.lyric.map(({ content, time }, index) => {
return (
<motion.div
id={String(time)}
key={`${String(index)}-${String(time)}`}
className={cx(
'max-w-[78%] py-[calc(100vw_*_0.0111)] text-white duration-700'
)}
style={{
y,
opacity:
index === currentIndex
? 1
: index > currentIndex && index < currentIndex + 8
? 0.2
: 0,
transitionProperty:
index > currentIndex - 2 && index < currentIndex + 8
? 'transform, opacity'
: 'none',
transitionTimingFunction:
index > currentIndex - 2 && index < currentIndex + 8
? 'cubic-bezier(0.5, 0.2, 0.2, 0.8)'
: 'none',
transitionDelay: `${
index < currentIndex + 8 && index > currentIndex
? 0.04 * (index - currentIndex)
: 0
}s`,
}}
>
{content}
</motion.div>
)
})}
</div>
)
}
export default Lyric

View file

@ -0,0 +1,68 @@
import Player from './Player'
import { player, state } from '@/web/store'
import { getCoverColor } from '@/web/utils/common'
import { colord } from 'colord'
import IconButton from '../IconButton'
import SvgIcon from '../SvgIcon'
import Lyric from './Lyric'
import { motion, AnimatePresence } from 'framer-motion'
import Lyric2 from './Lyric2'
import useCoverColor from '@/web/hooks/useCoverColor'
import cx from 'classnames'
import { useMemo } from 'react'
import { useSnapshot } from 'valtio'
const LyricPanel = () => {
const stateSnapshot = useSnapshot(state)
const playerSnapshot = useSnapshot(player)
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
const bgColor = useCoverColor(track?.al?.picUrl ?? '')
return (
<AnimatePresence>
{stateSnapshot.uiStates.showLyricPanel && (
<motion.div
initial={{
y: '100%',
}}
animate={{
y: 0,
transition: {
ease: 'easeIn',
duration: 0.4,
},
}}
exit={{
y: '100%',
transition: {
ease: 'easeIn',
duration: 0.4,
},
}}
className={cx(
'fixed inset-0 z-40 grid grid-cols-[repeat(13,_minmax(0,_1fr))] gap-[8%] bg-gray-800'
)}
style={{
background: `linear-gradient(to bottom, ${bgColor.from}, ${bgColor.to})`,
}}
>
{/* Drag area */}
<div className='app-region-drag absolute top-0 right-0 left-0 h-16'></div>
<Player className='col-span-6' />
{/* <Lyric className='col-span-7' /> */}
<Lyric2 className='col-span-7' />
<div className='absolute bottom-3.5 right-7 text-white'>
<IconButton onClick={() => (state.uiStates.showLyricPanel = false)}>
<SvgIcon className='h-6 w-6' name='lyrics' />
</IconButton>
</div>
</motion.div>
)}
</AnimatePresence>
)
}
export default LyricPanel

View file

@ -0,0 +1,168 @@
import useUserLikedTracksIDs, {
useMutationLikeATrack,
} from '@/web/hooks/useUserLikedTracksIDs'
import { player, state } from '@/web/store'
import { resizeImage } from '@/web/utils/common'
import ArtistInline from '../ArtistsInline'
import Cover from '../Cover'
import IconButton from '../IconButton'
import SvgIcon from '../SvgIcon'
import {
State as PlayerState,
Mode as PlayerMode,
} from '@/web/utils/player'
import { useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import { useSnapshot } from 'valtio'
import cx from 'classnames'
const PlayingTrack = () => {
const playerSnapshot = useSnapshot(player)
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
const navigate = useNavigate()
const toAlbum = () => {
const id = track?.al?.id
if (!id) return
navigate(`/album/${id}`)
state.uiStates.showLyricPanel = false
}
const trackListSource = useMemo(
() => playerSnapshot.trackListSource,
[playerSnapshot.trackListSource]
)
const hasListSource =
playerSnapshot.mode !== PlayerMode.FM && trackListSource?.type
const toTrackListSource = () => {
if (!hasListSource) return
navigate(`/${trackListSource.type}/${trackListSource.id}`)
state.uiStates.showLyricPanel = false
}
const toArtist = (id: number) => {
navigate(`/artist/${id}`)
state.uiStates.showLyricPanel = false
}
return (
<div>
<div
onClick={toTrackListSource}
className={cx(
'line-clamp-1 text-[22px] font-semibold text-white',
hasListSource && 'hover:underline'
)}
>
{track?.name}
</div>
<div className='line-clamp-1 -mt-0.5 inline-flex max-h-7 text-white opacity-60'>
<ArtistInline artists={track?.ar ?? []} onClick={toArtist} />
{!!track?.al?.id && (
<span>
{' '}
-{' '}
<span onClick={toAlbum} className='hover:underline'>
{track?.al.name}
</span>
</span>
)}
</div>
</div>
)
}
const LikeButton = ({ track }: { track: Track | undefined | null }) => {
const { data: userLikedSongs } = useUserLikedTracksIDs()
const mutationLikeATrack = useMutationLikeATrack()
return (
<div className='mr-1 '>
<IconButton
onClick={() => track?.id && mutationLikeATrack.mutate(track.id)}
>
<SvgIcon
className='h-6 w-6 text-white'
name={
track?.id && userLikedSongs?.ids?.includes(track.id)
? 'heart'
: 'heart-outline'
}
/>
</IconButton>
</div>
)
}
const Controls = () => {
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])
return (
<div className='flex items-center justify-center gap-2 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>
)}
<IconButton
onClick={() => track && player.playOrPause()}
disabled={!track}
className='after:rounded-xl'
>
<SvgIcon
className='h-7 w-7'
name={
[PlayerState.Playing, PlayerState.Loading].includes(state)
? 'pause'
: 'play'
}
/>
</IconButton>
<IconButton onClick={() => track && player.nextTrack()} disabled={!track}>
<SvgIcon className='h-6 w-6' name='next' />
</IconButton>
</div>
)
}
const Player = ({ className }: { className?: string }) => {
const playerSnapshot = useSnapshot(player)
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
return (
<div className={cx('flex w-full items-center justify-end', className)}>
<div className='relative w-[74%]'>
<Cover
imageUrl={resizeImage(track?.al.picUrl ?? '', 'lg')}
roundedClass='rounded-2xl'
alwaysShowShadow={true}
/>
<div className='absolute -bottom-32 right-0 left-0'>
<div className='mt-6 flex cursor-default justify-between'>
<PlayingTrack />
<LikeButton track={track} />
</div>
<Controls />
</div>
</div>
</div>
)
}
export default Player

View file

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