feat: adjust live photo interaction (#97)

This commit is contained in:
Wenzhuo Liu
2025-09-08 20:28:49 +08:00
committed by GitHub
parent 7f07497553
commit 888a039e75
11 changed files with 76 additions and 42 deletions

View File

@@ -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>

View File

@@ -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,

View File

@@ -314,6 +314,7 @@ export const PhotoViewer = ({
// Live Photo props
isLivePhoto={photo.isLivePhoto}
livePhotoVideoUrl={photo.livePhotoVideoUrl}
shouldAutoPlayLivePhotoOnce={isCurrentImage}
// HDR props
isHDR={photo.isHDR}
/>

View File

@@ -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>

View File

@@ -28,6 +28,7 @@ export interface ProgressiveImageProps {
// Live Photo 相关 props
isLivePhoto?: boolean
livePhotoVideoUrl?: string
shouldAutoPlayLivePhotoOnce?: boolean
// HDR 相关 props
isHDR?: boolean