feat: updates

This commit is contained in:
qier222 2023-01-07 14:39:03 +08:00
parent 884f3df41a
commit c6c59b2cd9
No known key found for this signature in database
84 changed files with 3531 additions and 2616 deletions

View file

@ -0,0 +1,260 @@
import useUserVideos, { useMutationLikeAVideo } from '@/web/api/hooks/useUserVideos'
import player from '@/web/states/player'
import uiStates from '@/web/states/uiStates'
import { formatDuration } from '@/web/utils/common'
import { css, cx } from '@emotion/css'
import { motion, useAnimationControls } from 'framer-motion'
import React, { useEffect, useMemo, useRef } from 'react'
import Icon from '../Icon'
import Slider from '../Slider'
import { proxy, useSnapshot } from 'valtio'
import { throttle } from 'lodash-es'
const videoStates = proxy({
currentTime: 0,
duration: 0,
isPaused: true,
isFullscreen: false,
})
const VideoInstance = ({ src, poster }: { src: string; poster: string }) => {
const videoRef = useRef<HTMLVideoElement>(null)
const videoContainerRef = useRef<HTMLDivElement>(null)
const video = videoRef.current
const { isPaused, isFullscreen } = useSnapshot(videoStates)
useEffect(() => {
if (!video || !src) return
const handleDurationChange = () => (videoStates.duration = video.duration * 1000)
const handleTimeUpdate = () => (videoStates.currentTime = video.currentTime * 1000)
const handleFullscreenChange = () => (videoStates.isFullscreen = !!document.fullscreenElement)
const handlePause = () => (videoStates.isPaused = true)
const handlePlay = () => (videoStates.isPaused = false)
video.addEventListener('timeupdate', handleTimeUpdate)
video.addEventListener('durationchange', handleDurationChange)
document.addEventListener('fullscreenchange', handleFullscreenChange)
video.addEventListener('pause', handlePause)
video.addEventListener('play', handlePlay)
return () => {
video.removeEventListener('timeupdate', handleTimeUpdate)
video.removeEventListener('durationchange', handleDurationChange)
document.removeEventListener('fullscreenchange', handleFullscreenChange)
video.removeEventListener('pause', handlePause)
video.removeEventListener('play', handlePlay)
}
})
// if video is playing, pause music
useEffect(() => {
if (!isPaused) player.pause()
}, [isPaused])
const togglePlay = () => {
videoStates.isPaused ? videoRef.current?.play() : videoRef.current?.pause()
}
const toggleFullscreen = async () => {
if (document.fullscreenElement) {
document.exitFullscreen()
videoStates.isFullscreen = false
} else {
if (videoContainerRef.current) {
videoContainerRef.current.requestFullscreen()
videoStates.isFullscreen = true
}
}
}
// reset video state when src changes
useEffect(() => {
videoStates.currentTime = 0
videoStates.duration = 0
videoStates.isPaused = true
videoStates.isFullscreen = false
}, [src])
// animation controls
const animationControls = useAnimationControls()
const controlsTimestamp = useRef(0)
const isControlsVisible = useRef(false)
const isMouseOverControls = useRef(false)
// hide controls after 2 seconds
const showControls = () => {
isControlsVisible.current = true
controlsTimestamp.current = Date.now()
animationControls.start('visible')
}
const hideControls = () => {
isControlsVisible.current = false
animationControls.start('hidden')
}
useEffect(() => {
if (!isFullscreen) return
const interval = setInterval(() => {
if (
isControlsVisible.current &&
Date.now() - controlsTimestamp.current > 2000 &&
!isMouseOverControls.current
) {
hideControls()
}
}, 300)
return () => clearInterval(interval)
}, [isFullscreen])
if (!src) return null
return (
<motion.div
initial='hidden'
animate={animationControls}
ref={videoContainerRef}
className={cx(
'relative aspect-video overflow-hidden rounded-24 bg-black',
css`
video::-webkit-media-controls {
display: none !important;
}
`,
!isFullscreen &&
css`
height: 60vh;
`
)}
onClick={togglePlay}
onMouseOver={showControls}
onMouseOut={hideControls}
onMouseMove={() => !isControlsVisible.current && isFullscreen && showControls()}
>
<video ref={videoRef} src={src} controls={false} poster={poster} className='h-full w-full' />
<Controls
videoRef={videoRef}
toggleFullscreen={toggleFullscreen}
togglePlay={togglePlay}
onMouseOver={() => (isMouseOverControls.current = true)}
onMouseOut={() => (isMouseOverControls.current = false)}
/>
</motion.div>
)
}
const Controls = ({
videoRef,
toggleFullscreen,
togglePlay,
onMouseOver,
onMouseOut,
}: {
videoRef: React.RefObject<HTMLVideoElement>
toggleFullscreen: () => void
togglePlay: () => void
onMouseOver: () => void
onMouseOut: () => void
}) => {
const video = videoRef.current
const { playingVideoID } = useSnapshot(uiStates)
const { currentTime, duration, isPaused, isFullscreen } = useSnapshot(videoStates)
const { data: likedVideos } = useUserVideos()
const isLiked = useMemo(() => {
return !!likedVideos?.data?.find(video => String(video.vid) === String(playingVideoID))
}, [likedVideos])
const likeAVideo = useMutationLikeAVideo()
const onLike = async () => {
if (playingVideoID) likeAVideo.mutateAsync(playingVideoID)
}
// keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
toggleFullscreen()
break
case ' ':
togglePlay()
break
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [])
const animationVariants = {
hidden: { y: '48px', opacity: 0 },
visible: { y: 0, opacity: 1 },
}
const animationTransition = { type: 'spring', bounce: 0.4, duration: 0.5 }
return (
<div onClick={e => e.stopPropagation()} onMouseOver={onMouseOver} onMouseOut={onMouseOut}>
{/* Current Time */}
<motion.div
variants={animationVariants}
transition={animationTransition}
className={cx(
'pointer-events-none absolute left-5 cursor-default select-none font-extrabold text-white/40',
css`
bottom: 100px;
font-size: 120px;
line-height: 120%;
letter-spacing: 0.02em;
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: rgba(255, 255, 255, 0.7);
`
)}
>
{formatDuration(currentTime || 0, 'en-US', 'hh:mm:ss')}
</motion.div>
{/* Controls */}
<motion.div
variants={{
hidden: { y: '48px', opacity: 0 },
visible: { y: 0, opacity: 1 },
}}
transition={animationTransition}
className='absolute bottom-5 left-5 flex rounded-20 bg-black/70 py-3 px-5 backdrop-blur-3xl'
>
<button
onClick={togglePlay}
className='flex h-11 w-11 items-center justify-center rounded-full bg-white/20 text-white/80 transition-colors duration-400 hover:bg-white/30'
>
<Icon name={isPaused ? 'play' : 'pause'} className='h-6 w-6' />
</button>
<button
onClick={onLike}
className='ml-3 flex h-11 w-11 items-center justify-center rounded-full bg-white/20 text-white/80 transition-colors duration-400 hover:bg-white/30'
>
<Icon name={isLiked ? 'heart' : 'heart-outline'} className='h-6 w-6' />
</button>
<button
onClick={toggleFullscreen}
className='ml-3 flex h-11 w-11 items-center justify-center rounded-full bg-white/20 text-white/80 transition-colors duration-400 hover:bg-white/30'
>
<Icon name={isFullscreen ? 'fullscreen-exit' : 'fullscreen-enter'} className='h-6 w-6' />
</button>
{/* Slider */}
<div className='ml-5 flex items-center'>
<div
className={css`
width: 214px;
`}
>
<Slider
min={0}
max={duration || 99999}
value={currentTime || 0}
onChange={value => video?.currentTime && (video.currentTime = value)}
onlyCallOnChangeAfterDragEnded={true}
/>
</div>
{/* Duration */}
<span className='ml-4 text-14 font-bold text-white/20'>
{formatDuration(duration || 0, 'en-US', 'hh:mm:ss')}
</span>
</div>
</motion.div>
</div>
)
}
export default VideoInstance

View file

@ -0,0 +1,117 @@
import { css, cx } from '@emotion/css'
import { createPortal } from 'react-dom'
import useMV, { useMVUrl } from '../../api/hooks/useMV'
import { AnimatePresence, motion } from 'framer-motion'
import { ease } from '@/web/utils/const'
import Icon from '../Icon'
import VideoInstance from './VideoInstance'
import { toHttps } from '@/web/utils/common'
import uiStates, { closeVideoPlayer } from '@/web/states/uiStates'
import { useSnapshot } from 'valtio'
import { useNavigate } from 'react-router-dom'
const VideoPlayer = () => {
const { playingVideoID } = useSnapshot(uiStates)
const { fullscreen } = useSnapshot(uiStates)
const navigate = useNavigate()
const { data: mv, isLoading } = useMV({ mvid: playingVideoID || 0 })
const { data: mvDetails } = useMVUrl({ id: playingVideoID || 0 })
const mvUrl = toHttps(mvDetails?.data?.url)
const poster = toHttps(mv?.data.cover)
return createPortal(
<AnimatePresence>
{playingVideoID && (
<div
id='video-player'
className={cx(
'fixed inset-0 z-20 flex select-none items-center justify-center overflow-hidden',
window.env?.isElectron && !fullscreen && 'rounded-24'
)}
>
{/* Blur bg */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ ease, duration: 0.5 }}
className='absolute inset-0 bg-gray-50/80 backdrop-blur-3xl'
></motion.div>
<motion.div
variants={{
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.3,
ease,
delay: 0.2,
},
},
hidden: {
opacity: 0,
y: 100,
transition: {
duration: 0.3,
ease,
},
},
}}
initial='hidden'
animate='visible'
exit='hidden'
className='relative'
>
{/* Video Title */}
<div className='absolute -top-16 flex cursor-default text-32 font-medium text-white transition-all'>
{isLoading ? (
<span className='rounded-full bg-white/10 text-transparent'>PLACEHOLDER2023</span>
) : (
<>
<div className='line-clamp-1' title={mv?.data.artistName + ' - ' + mv?.data.name}>
<a
onClick={() => {
if (!mv?.data.artistId) return
closeVideoPlayer()
navigate('/artist/' + mv.data.artistId)
}}
className='transition duration-400 hover:underline'
>
{mv?.data.artistName}
</a>{' '}
- {mv?.data.name}
</div>
<div className='ml-4 text-white/20'>{mv?.data.publishTime.slice(0, 4)}</div>
</>
)}
</div>
{/* Video */}
<VideoInstance src={mvUrl} poster={poster} />
{/* Close button */}
<div className='absolute -bottom-24 flex w-full justify-center'>
<motion.div
layout='position'
transition={{ ease }}
onClick={() => {
const video = document.querySelector('#video-player video') as HTMLVideoElement
video?.pause()
closeVideoPlayer()
}}
className='flex h-14 w-14 items-center justify-center rounded-full bg-white/10 text-white/50 transition-colors duration-300 hover:bg-white/20 hover:text-white/70'
>
<Icon name='x' className='h-6 w-6' />
</motion.div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>,
document.body
)
}
export default VideoPlayer

View file

@ -0,0 +1,2 @@
import VideoPlayer from './VideoPlayer'
export default VideoPlayer