import React, { useState, useRef } from 'react' import { Play, Lock, Download, ImageOff } from 'lucide-react' import { LivePhotoIcon } from '../../components/LivePhotoIcon' import { RefreshCw } from 'lucide-react' interface SnsMedia { url: string thumb: string md5?: string token?: string key?: string encIdx?: string livePhoto?: { url: string thumb: string token?: string key?: string encIdx?: string } } interface SnsMediaGridProps { mediaList: SnsMedia[] postType?: number onPreview: (src: string, isVideo?: boolean, liveVideoPath?: string) => void onMediaDeleted?: () => void } const isSnsVideoUrl = (url?: string): boolean => { if (!url) return false const lower = url.toLowerCase() return (lower.includes('snsvideodownload') || lower.includes('.mp4') || lower.includes('video')) && !lower.includes('vweixinthumb') } const extractVideoFrame = async (videoPath: string): Promise => { return new Promise((resolve, reject) => { const video = document.createElement('video') video.preload = 'auto' video.src = videoPath video.muted = true video.currentTime = 0 // Initial reset // video.crossOrigin = 'anonymous' // Not needed for file:// usually const onSeeked = () => { try { const canvas = document.createElement('canvas') canvas.width = video.videoWidth canvas.height = video.videoHeight const ctx = canvas.getContext('2d') if (ctx) { ctx.drawImage(video, 0, 0, canvas.width, canvas.height) const dataUrl = canvas.toDataURL('image/jpeg', 0.8) resolve(dataUrl) } else { reject(new Error('Canvas context failed')) } } catch (e) { reject(e) } finally { // Cleanup video.removeEventListener('seeked', onSeeked) video.src = '' video.load() } } video.onloadedmetadata = () => { if (video.duration === Infinity || isNaN(video.duration)) { // Determine duration failed, try a fixed small offset video.currentTime = 1 } else { video.currentTime = Math.max(0.1, video.duration / 2) } } video.onseeked = onSeeked video.onerror = (e) => { reject(new Error('Video load failed')) } }) } const MediaItem = ({ media, postType, onPreview, onMediaDeleted }: { media: SnsMedia; postType?: number; onPreview: (src: string, isVideo?: boolean, liveVideoPath?: string) => void; onMediaDeleted?: () => void }) => { const [error, setError] = useState(false) const [deleted, setDeleted] = useState(false) const [loading, setLoading] = useState(true) const markDeleted = () => { setDeleted(true); onMediaDeleted?.() } const retryCount = useRef(0) const [retryKey, setRetryKey] = useState(0) const [thumbSrc, setThumbSrc] = useState('') const [videoPath, setVideoPath] = useState('') const [liveVideoPath, setLiveVideoPath] = useState('') const [isDecrypting, setIsDecrypting] = useState(false) const [isGeneratingCover, setIsGeneratingCover] = useState(false) const isVideo = isSnsVideoUrl(media.url) const isLive = !!media.livePhoto const targetUrl = media.thumb || media.url // type 7 的朋友圈媒体不需要解密,直接使用原始 URL const skipDecrypt = postType === 7 // 视频重试:失败时重试最多2次,耗尽才标记删除 const videoRetryOrDelete = () => { if (retryCount.current < 2) { retryCount.current++ setRetryKey(k => k + 1) } else { markDeleted() } } // Simple effect to load image/decrypt // Simple effect to load image/decrypt React.useEffect(() => { let cancelled = false setLoading(true) const load = async () => { try { if (!isVideo) { // For images, we proxy to get the local path/base64 const result = await window.electronAPI.sns.proxyImage({ url: targetUrl, key: skipDecrypt ? undefined : media.key }) if (cancelled) return if (result.success) { if (result.dataUrl) setThumbSrc(result.dataUrl) else if (result.videoPath) setThumbSrc(`file://${result.videoPath.replace(/\\/g, '/')}`) } else { markDeleted() } // Pre-load live photo video if needed if (isLive && media.livePhoto?.url) { window.electronAPI.sns.proxyImage({ url: media.livePhoto.url, key: skipDecrypt ? undefined : (media.livePhoto.key || media.key) }).then((res: any) => { if (!cancelled && res.success && res.videoPath) { setLiveVideoPath(`file://${res.videoPath.replace(/\\/g, '/')}`) } }).catch(() => { }) } setLoading(false) } else { // Video logic: Decrypt -> Extract Frame setIsGeneratingCover(true) // First check if we already have it decryptable? // Usually we need to call proxyImage with the video URL to decrypt it to cache const result = await window.electronAPI.sns.proxyImage({ url: media.url, key: skipDecrypt ? undefined : media.key }) if (cancelled) return if (result.success && result.videoPath) { const localPath = `file://${result.videoPath.replace(/\\/g, '/')}` setVideoPath(localPath) try { const coverDataUrl = await extractVideoFrame(localPath) if (!cancelled) setThumbSrc(coverDataUrl) } catch (err) { console.error('Frame extraction failed', err) // 封面提取失败,用视频路径作为 fallback,让