mixvideo-v2/apps/desktop/src/components/MaterialThumbnail.tsx

246 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { FileVideo, FileAudio, FileImage, File, Loader2, Play, Pause, Volume2 } from 'lucide-react';
import { Material } from '../types/material';
import { invoke } from '@tauri-apps/api/core';
import { useLazyLoad } from '../hooks/useLazyLoad';
interface MaterialThumbnailProps {
material: Material;
size?: 'small' | 'medium' | 'large';
className?: string;
thumbnailCache?: Map<string, string>;
setThumbnailCache?: (cache: Map<string, string>) => void;
}
/**
* Material缩略图组件
* 遵循Tauri开发规范的组件设计模式
* 支持懒加载、缓存机制、错误处理
*/
export const MaterialThumbnail: React.FC<MaterialThumbnailProps> = ({
material,
size = 'medium',
className = '',
thumbnailCache = new Map(),
setThumbnailCache = () => {},
}) => {
const [loading, setLoading] = useState(false);
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
const [error, setError] = useState(false);
const [isPlaying, setIsPlaying] = useState(false);
const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(null);
// 使用懒加载Hook当缩略图容器可见时才开始加载
const { isVisible, elementRef } = useLazyLoad(0.1, '100px');
// 根据size确定尺寸
const getSizeClasses = () => {
switch (size) {
case 'small':
return 'w-12 h-12';
case 'medium':
return 'w-16 h-16';
case 'large':
return 'w-24 h-24';
default:
return 'w-16 h-16';
}
};
// 获取文件类型图标
const getTypeIcon = () => {
const iconSize = size === 'small' ? 'w-6 h-6' : size === 'large' ? 'w-12 h-12' : 'w-8 h-8';
switch (material.material_type) {
case 'Video':
return <FileVideo className={`${iconSize} text-blue-500`} />;
case 'Audio':
return <FileAudio className={`${iconSize} text-green-500`} />;
case 'Image':
return <FileImage className={`${iconSize} text-purple-500`} />;
default:
return <File className={`${iconSize} text-gray-500`} />;
}
};
// 音频播放控制
const toggleAudioPlay = async () => {
if (material.material_type !== 'Audio') return;
if (!audioElement) {
try {
// 通过后端API获取音频数据
console.log('获取音频数据:', material.id);
const audioDataUrl = await invoke<string>('get_audio_file_base64', {
materialId: material.id
});
console.log('获取音频数据成功');
// 创建音频元素
const audio = new Audio();
audio.src = audioDataUrl;
audio.onended = () => setIsPlaying(false);
audio.onerror = () => {
console.error('音频播放失败');
setIsPlaying(false);
};
setAudioElement(audio);
await audio.play();
setIsPlaying(true);
} catch (error) {
console.error('音频播放失败:', error);
setIsPlaying(false);
}
} else {
if (isPlaying) {
audioElement.pause();
setIsPlaying(false);
} else {
try {
await audioElement.play();
setIsPlaying(true);
} catch (error) {
console.error('音频播放失败:', error);
setIsPlaying(false);
}
}
}
};
// 清理音频元素
useEffect(() => {
return () => {
if (audioElement) {
audioElement.pause();
audioElement.src = '';
}
};
}, [audioElement]);
useEffect(() => {
// 只有当元素可见时才加载内容
if (!isVisible) return;
const loadContent = async () => {
const materialId = material.id;
// 检查缓存
if (thumbnailCache.has(materialId)) {
const cachedUrl = thumbnailCache.get(materialId);
setThumbnailUrl(cachedUrl || null);
return;
}
// 根据素材类型加载不同内容
if (material.material_type === 'Video') {
// 视频:生成缩略图
setLoading(true);
setError(false);
try {
console.log('获取视频缩略图:', materialId);
const dataUrl = await invoke<string>('get_material_thumbnail_base64', {
materialId: materialId
});
console.log('获取缩略图成功');
setThumbnailUrl(dataUrl);
// 更新缓存
const newCache = new Map(thumbnailCache);
newCache.set(materialId, dataUrl);
setThumbnailCache(newCache);
} catch (error) {
console.error('获取缩略图失败:', error);
setError(true);
} finally {
setLoading(false);
}
} else if (material.material_type === 'Image') {
// 图片通过后端API获取base64数据
setLoading(true);
setError(false);
try {
console.log('获取图片数据:', materialId);
const dataUrl = await invoke<string>('get_material_thumbnail_base64', {
materialId: materialId
});
console.log('获取图片数据成功');
setThumbnailUrl(dataUrl);
// 更新缓存
const newCache = new Map(thumbnailCache);
newCache.set(materialId, dataUrl);
setThumbnailCache(newCache);
} catch (error) {
console.error('获取图片数据失败:', error);
setError(true);
} finally {
setLoading(false);
}
}
// 音频类型不需要加载缩略图,直接显示播放控件
};
loadContent();
}, [isVisible, material.id, material.material_type, material.original_path, thumbnailCache, setThumbnailCache]);
// 渲染音频播放控件
const renderAudioPlayer = () => {
const iconSize = size === 'small' ? 'w-4 h-4' : size === 'large' ? 'w-8 h-8' : 'w-6 h-6';
return (
<div className="relative w-full h-full bg-gradient-to-br from-green-50 to-green-100 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:from-green-100 hover:to-green-200 transition-colors"
onClick={toggleAudioPlay}>
<Volume2 className={`${iconSize} text-green-600 mb-1`} />
<div className="flex items-center justify-center">
{isPlaying ? (
<Pause className={`${size === 'small' ? 'w-3 h-3' : size === 'large' ? 'w-5 h-5' : 'w-4 h-4'} text-green-700`} />
) : (
<Play className={`${size === 'small' ? 'w-3 h-3' : size === 'large' ? 'w-5 h-5' : 'w-4 h-4'} text-green-700`} />
)}
</div>
{size !== 'small' && (
<div className="text-xs text-green-600 mt-1 font-medium">
{isPlaying ? '播放中' : '点击播放'}
</div>
)}
</div>
);
};
return (
<div
ref={elementRef}
className={`${getSizeClasses()} bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden ${className}`}
>
{loading ? (
<Loader2 className={`${size === 'small' ? 'w-3 h-3' : size === 'large' ? 'w-6 h-6' : 'w-4 h-4'} animate-spin text-blue-600`} />
) : material.material_type === 'Audio' ? (
// 音频:显示播放控件
renderAudioPlayer()
) : thumbnailUrl && !error ? (
// 图片和视频:显示图片/缩略图
<img
src={thumbnailUrl}
alt={`${material.name} ${material.material_type === 'Image' ? '图片' : '缩略图'}`}
className="w-full h-full object-cover rounded-lg"
onError={() => {
setError(true);
setThumbnailUrl(null);
}}
/>
) : isVisible ? (
// 加载失败或其他类型:显示类型图标
getTypeIcon()
) : (
// 未加载时显示占位符
<div className={`${size === 'small' ? 'w-6 h-6' : size === 'large' ? 'w-12 h-12' : 'w-8 h-8'} bg-gray-200 rounded flex items-center justify-center`}>
<div className={`${size === 'small' ? 'w-3 h-3' : size === 'large' ? 'w-6 h-6' : 'w-4 h-4'} bg-gray-300 rounded`}></div>
</div>
)}
</div>
);
};