diff --git a/src/pages/ImageWindow.scss b/src/pages/ImageWindow.scss index 3004bbf..c1d842d 100644 --- a/src/pages/ImageWindow.scss +++ b/src/pages/ImageWindow.scss @@ -46,6 +46,18 @@ background: var(--bg-tertiary); color: var(--text-primary); } + + &:disabled { + cursor: default; + opacity: 1; + } + + &.live-play-btn { + &.active { + background: rgba(var(--primary-rgb, 76, 132, 255), 0.16); + color: var(--primary, #4c84ff); + } + } } .scale-text { @@ -78,14 +90,40 @@ cursor: grabbing; } - img, video { + .media-wrapper { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + will-change: transform; + } + + img, + video { + display: block; max-width: none; max-height: none; object-fit: contain; - will-change: transform; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); pointer-events: auto; } + + .live-video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: fill; + pointer-events: none; + opacity: 0; + will-change: opacity; + transition: opacity 0.3s ease-in-out; + } + + .live-video.visible { + opacity: 1; + } } } diff --git a/src/pages/ImageWindow.tsx b/src/pages/ImageWindow.tsx index 9e5b4eb..e6b2e5d 100644 --- a/src/pages/ImageWindow.tsx +++ b/src/pages/ImageWindow.tsx @@ -1,4 +1,3 @@ - import { useState, useEffect, useRef, useCallback } from 'react' import { useSearchParams } from 'react-router-dom' import { ZoomIn, ZoomOut, RotateCw, RotateCcw } from 'lucide-react' @@ -9,15 +8,19 @@ export default function ImageWindow() { const [searchParams] = useSearchParams() const imagePath = searchParams.get('imagePath') const liveVideoPath = searchParams.get('liveVideoPath') - const [showLive, setShowLive] = useState(false) + const hasLiveVideo = !!liveVideoPath + + const [isPlayingLive, setIsPlayingLive] = useState(false) + const [isVideoVisible, setIsVideoVisible] = useState(false) const videoRef = useRef(null) + const liveCleanupTimerRef = useRef(null) + const [scale, setScale] = useState(1) const [rotation, setRotation] = useState(0) const [position, setPosition] = useState({ x: 0, y: 0 }) const [initialScale, setInitialScale] = useState(1) - const [imgNatural, setImgNatural] = useState({ w: 0, h: 0 }) const viewportRef = useRef(null) - + // 使用 ref 存储拖动状态,避免闭包问题 const dragStateRef = useRef({ isDragging: false, @@ -27,11 +30,49 @@ export default function ImageWindow() { startPosY: 0 }) + const clearLiveCleanupTimer = useCallback(() => { + if (liveCleanupTimerRef.current !== null) { + window.clearTimeout(liveCleanupTimerRef.current) + liveCleanupTimerRef.current = null + } + }, []) + + const stopLivePlayback = useCallback((immediate = false) => { + clearLiveCleanupTimer() + setIsVideoVisible(false) + + if (immediate) { + if (videoRef.current) { + videoRef.current.pause() + videoRef.current.currentTime = 0 + } + setIsPlayingLive(false) + return + } + + liveCleanupTimerRef.current = window.setTimeout(() => { + if (videoRef.current) { + videoRef.current.pause() + videoRef.current.currentTime = 0 + } + setIsPlayingLive(false) + liveCleanupTimerRef.current = null + }, 300) + }, [clearLiveCleanupTimer]) + + const handlePlayLiveVideo = useCallback(() => { + if (!liveVideoPath || isPlayingLive) return + + clearLiveCleanupTimer() + setIsPlayingLive(true) + setIsVideoVisible(false) + }, [clearLiveCleanupTimer, liveVideoPath, isPlayingLive]) + const handleZoomIn = () => setScale(prev => Math.min(prev + 0.25, 10)) const handleZoomOut = () => setScale(prev => Math.max(prev - 0.25, 0.1)) const handleRotate = () => setRotation(prev => (prev + 90) % 360) const handleRotateCcw = () => setRotation(prev => (prev - 90 + 360) % 360) - + // 重置视图 const handleReset = useCallback(() => { setScale(1) @@ -44,8 +85,7 @@ export default function ImageWindow() { const img = e.currentTarget const naturalWidth = img.naturalWidth const naturalHeight = img.naturalHeight - setImgNatural({ w: naturalWidth, h: naturalHeight }) - + if (viewportRef.current) { const viewportWidth = viewportRef.current.clientWidth * 0.9 const viewportHeight = viewportRef.current.clientHeight * 0.9 @@ -57,14 +97,37 @@ export default function ImageWindow() { } }, []) + // 视频挂载后再播放,避免点击瞬间 ref 尚未就绪导致丢播 + useEffect(() => { + if (!isPlayingLive || !videoRef.current) return + + const timer = window.setTimeout(() => { + const video = videoRef.current + if (!video || !isPlayingLive || !video.paused) return + + video.currentTime = 0 + void video.play().catch(() => { + stopLivePlayback(true) + }) + }, 0) + + return () => window.clearTimeout(timer) + }, [isPlayingLive, stopLivePlayback]) + + useEffect(() => { + return () => { + clearLiveCleanupTimer() + } + }, [clearLiveCleanupTimer]) + // 使用原生事件监听器处理拖动 useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (!dragStateRef.current.isDragging) return - + const dx = e.clientX - dragStateRef.current.startX const dy = e.clientY - dragStateRef.current.startY - + setPosition({ x: dragStateRef.current.startPosX + dx, y: dragStateRef.current.startPosY + dy @@ -88,7 +151,7 @@ export default function ImageWindow() { const handleMouseDown = (e: React.MouseEvent) => { if (e.button !== 0) return e.preventDefault() - + dragStateRef.current = { isDragging: true, startX: e.clientX, @@ -112,15 +175,25 @@ export default function ImageWindow() { // 快捷键支持 useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') window.electronAPI.window.close() + if (e.key === 'Escape') { + if (isPlayingLive) { + stopLivePlayback(true) + return + } + window.electronAPI.window.close() + } if (e.key === '=' || e.key === '+') handleZoomIn() if (e.key === '-') handleZoomOut() if (e.key === 'r' || e.key === 'R') handleRotate() if (e.key === '0') handleReset() + if (e.key === ' ' && hasLiveVideo) { + e.preventDefault() + handlePlayLiveVideo() + } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) - }, [handleReset]) + }, [handleReset, hasLiveVideo, handlePlayLiveVideo, isPlayingLive, stopLivePlayback]) if (!imagePath) { return ( @@ -137,22 +210,19 @@ export default function ImageWindow() {
- {liveVideoPath && ( - + {hasLiveVideo && ( + <> + +
+ )} {Math.round(displayScale * 100)}% @@ -170,32 +240,31 @@ export default function ImageWindow() { onDoubleClick={handleDoubleClick} onMouseDown={handleMouseDown} > - {liveVideoPath && ( -
)