mirror of
https://github.com/Afilmory/afilmory
synced 2026-04-24 23:05:05 +00:00
feat: adjust live photo interaction (#97)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { AnimatePresence, m } from 'motion/react'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { clsxm } from '~/lib/cn'
|
||||
@@ -13,7 +13,6 @@ export const LivePhotoBadge: FC<LivePhotoBadgeProps> = ({
|
||||
isLivePhotoPlaying,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const hoverTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const handlePlay = useCallback(async () => {
|
||||
if (!livePhotoRef.current?.getIsVideoLoaded() || isLivePhotoPlaying) return
|
||||
@@ -25,18 +24,15 @@ export const LivePhotoBadge: FC<LivePhotoBadgeProps> = ({
|
||||
livePhotoRef.current?.stop()
|
||||
}, [livePhotoRef, isLivePhotoPlaying])
|
||||
|
||||
// Desktop hover logic
|
||||
const handleBadgeMouseEnter = useCallback(() => {
|
||||
if (isMobileDevice) return
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current)
|
||||
hoverTimerRef.current = setTimeout(handlePlay, 200)
|
||||
}, [handlePlay])
|
||||
const handleClick = useCallback(() => {
|
||||
if (!livePhotoRef.current?.getIsVideoLoaded()) return
|
||||
|
||||
const handleBadgeMouseLeave = useCallback(() => {
|
||||
if (isMobileDevice) return
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current)
|
||||
handleStop()
|
||||
}, [handleStop])
|
||||
if (isLivePhotoPlaying) {
|
||||
handleStop()
|
||||
} else {
|
||||
handlePlay()
|
||||
}
|
||||
}, [livePhotoRef, isLivePhotoPlaying, handlePlay, handleStop])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -44,13 +40,20 @@ export const LivePhotoBadge: FC<LivePhotoBadgeProps> = ({
|
||||
<div
|
||||
className={clsxm(
|
||||
'absolute z-20 flex items-center space-x-1 rounded-xl bg-black/50 px-1 py-1 text-xs text-white transition-all duration-200',
|
||||
!isMobileDevice && 'cursor-pointer hover:bg-black/70',
|
||||
'cursor-pointer hover:bg-black/70',
|
||||
isLivePhotoPlaying && 'bg-accent/70 hover:bg-accent/80',
|
||||
import.meta.env.DEV ? 'top-16 right-4' : 'top-12 lg:top-4 left-4',
|
||||
)}
|
||||
onMouseEnter={handleBadgeMouseEnter}
|
||||
onMouseLeave={handleBadgeMouseLeave}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<i className="i-mingcute-live-photo-line size-4" />
|
||||
<i
|
||||
className={clsxm(
|
||||
'size-4',
|
||||
isLivePhotoPlaying
|
||||
? 'i-mingcute-live-photo-fill'
|
||||
: 'i-mingcute-live-photo-line',
|
||||
)}
|
||||
/>
|
||||
<span className="mr-1">{t('photo.live.badge')}</span>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +67,7 @@ export const LivePhotoBadge: FC<LivePhotoBadgeProps> = ({
|
||||
className="pointer-events-none absolute bottom-4 left-1/2 z-20 -translate-x-1/2"
|
||||
>
|
||||
<div className="flex items-center gap-2 rounded bg-black/50 px-2 py-1 text-xs text-white">
|
||||
<i className="i-mingcute-live-photo-line" />
|
||||
<i className="i-mingcute-live-photo-fill" />
|
||||
<span>{t('photo.live.playing')}</span>
|
||||
</div>
|
||||
</m.div>
|
||||
|
||||
@@ -24,6 +24,8 @@ interface LivePhotoVideoProps {
|
||||
/** 自定义样式类名 */
|
||||
className?: string
|
||||
onPlayingChange?: (isPlaying: boolean) => void
|
||||
/** 是否自动播放一次 */
|
||||
shouldAutoPlayOnce?: boolean
|
||||
}
|
||||
|
||||
export interface LivePhotoVideoHandle {
|
||||
@@ -40,12 +42,14 @@ export const LivePhotoVideo = ({
|
||||
isCurrentImage,
|
||||
className,
|
||||
onPlayingChange,
|
||||
shouldAutoPlayOnce = false,
|
||||
}: LivePhotoVideoProps & {
|
||||
ref?: React.RefObject<LivePhotoVideoHandle | null>
|
||||
}) => {
|
||||
const [isPlayingLivePhoto, setIsPlayingLivePhoto] = useState(false)
|
||||
const [livePhotoVideoLoaded, setLivePhotoVideoLoaded] = useState(false)
|
||||
const [isConvertingVideo, setIsConvertingVideo] = useState(false)
|
||||
const hasAutoPlayedRef = useRef(false)
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const videoAnimateController = useAnimationControls()
|
||||
@@ -98,6 +102,7 @@ export const LivePhotoVideo = ({
|
||||
setIsPlayingLivePhoto(false)
|
||||
setLivePhotoVideoLoaded(false)
|
||||
setIsConvertingVideo(false)
|
||||
hasAutoPlayedRef.current = false
|
||||
|
||||
videoAnimateController.set({ opacity: 0 })
|
||||
}
|
||||
@@ -138,6 +143,28 @@ export const LivePhotoVideo = ({
|
||||
setIsPlayingLivePhoto(false)
|
||||
}, [isPlayingLivePhoto, videoAnimateController])
|
||||
|
||||
// Auto-play effect - play once when video is loaded
|
||||
useEffect(() => {
|
||||
if (
|
||||
shouldAutoPlayOnce &&
|
||||
isCurrentImage &&
|
||||
livePhotoVideoLoaded &&
|
||||
!isPlayingLivePhoto &&
|
||||
!isConvertingVideo &&
|
||||
!hasAutoPlayedRef.current
|
||||
) {
|
||||
hasAutoPlayedRef.current = true
|
||||
play()
|
||||
}
|
||||
}, [
|
||||
shouldAutoPlayOnce,
|
||||
isCurrentImage,
|
||||
livePhotoVideoLoaded,
|
||||
isPlayingLivePhoto,
|
||||
isConvertingVideo,
|
||||
play,
|
||||
])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
play,
|
||||
stop,
|
||||
|
||||
@@ -314,6 +314,7 @@ export const PhotoViewer = ({
|
||||
// Live Photo props
|
||||
isLivePhoto={photo.isLivePhoto}
|
||||
livePhotoVideoUrl={photo.livePhotoVideoUrl}
|
||||
shouldAutoPlayLivePhotoOnce={isCurrentImage}
|
||||
// HDR props
|
||||
isHDR={photo.isHDR}
|
||||
/>
|
||||
|
||||
@@ -40,6 +40,7 @@ export const ProgressiveImage = ({
|
||||
isCurrentImage = false,
|
||||
isLivePhoto = false,
|
||||
livePhotoVideoUrl,
|
||||
shouldAutoPlayLivePhotoOnce: shouldAutoPlayLivePhoto = false,
|
||||
isHDR = false,
|
||||
loadingIndicatorRef,
|
||||
}: ProgressiveImageProps) => {
|
||||
@@ -159,6 +160,7 @@ export const ProgressiveImage = ({
|
||||
loadingIndicatorRef={loadingIndicatorRef}
|
||||
isCurrentImage={isCurrentImage}
|
||||
onPlayingChange={setState.setIsLivePhotoPlaying}
|
||||
shouldAutoPlayOnce={shouldAutoPlayLivePhoto}
|
||||
/>
|
||||
)}
|
||||
</DOMImageViewer>
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface ProgressiveImageProps {
|
||||
// Live Photo 相关 props
|
||||
isLivePhoto?: boolean
|
||||
livePhotoVideoUrl?: string
|
||||
shouldAutoPlayLivePhotoOnce?: boolean
|
||||
|
||||
// HDR 相关 props
|
||||
isHDR?: boolean
|
||||
|
||||
Reference in New Issue
Block a user