mixvideo-v2/apps/desktop/src/pages/tools/WatermarkTool.tsx

242 lines
8.5 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 { ArrowLeft, Droplets, Upload, Settings } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { invoke } from '@tauri-apps/api/core';
import { Material } from '../../types/material';
import { WatermarkToolDialog } from '../../components/WatermarkToolDialog';
import { LoadingSpinner } from '../../components/LoadingSpinner';
/**
* 水印工具页面
* 提供水印检测、移除和添加功能
*/
const WatermarkTool: React.FC = () => {
const navigate = useNavigate();
// 状态管理
const [materials, setMaterials] = useState<Material[]>([]);
const [loading, setLoading] = useState(true);
const [selectedMaterials, setSelectedMaterials] = useState<string[]>([]);
const [showWatermarkDialog, setShowWatermarkDialog] = useState(false);
// 加载素材列表
useEffect(() => {
loadMaterials();
}, []);
const loadMaterials = async () => {
try {
setLoading(true);
const result = await invoke<Material[]>('get_all_materials');
setMaterials(result);
} catch (error) {
console.error('加载素材失败:', error);
} finally {
setLoading(false);
}
};
// 处理素材选择
const handleMaterialSelect = (materialId: string) => {
setSelectedMaterials(prev => {
if (prev.includes(materialId)) {
return prev.filter(id => id !== materialId);
} else {
return [...prev, materialId];
}
});
};
// 全选/取消全选
const handleSelectAll = () => {
if (selectedMaterials.length === materials.length) {
setSelectedMaterials([]);
} else {
setSelectedMaterials(materials.map(m => m.id));
}
};
// 打开水印工具对话框
const handleOpenWatermarkTool = () => {
if (selectedMaterials.length === 0) {
alert('请先选择要处理的素材');
return;
}
setShowWatermarkDialog(true);
};
// 格式化文件大小
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// 格式化时长
const formatDuration = (seconds: number) => {
if (!seconds) return '--';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
// 获取素材的时长信息
const getMaterialDuration = (material: Material): number | null => {
if (material.metadata === 'None') return null;
if ('Video' in material.metadata) return material.metadata.Video.duration;
if ('Audio' in material.metadata) return material.metadata.Audio.duration;
return null;
};
// 获取素材的尺寸信息
const getMaterialDimensions = (material: Material): { width: number; height: number } | null => {
if (material.metadata === 'None') return null;
if ('Video' in material.metadata) {
return { width: material.metadata.Video.width, height: material.metadata.Video.height };
}
if ('Image' in material.metadata) {
return { width: material.metadata.Image.width, height: material.metadata.Image.height };
}
return null;
};
return (
<div className="space-y-6">
{/* 页面头部 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/tools')}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<ArrowLeft className="w-5 h-5" />
</button>
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg">
<Droplets className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="text-gray-600"></p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={handleOpenWatermarkTool}
disabled={selectedMaterials.length === 0}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<Settings className="w-4 h-4" />
</button>
</div>
</div>
{/* 操作栏 */}
<div className="card p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={selectedMaterials.length === materials.length && materials.length > 0}
onChange={handleSelectAll}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm font-medium">
({selectedMaterials.length}/{materials.length})
</span>
</label>
</div>
<div className="text-sm text-gray-600">
{materials.length}
</div>
</div>
</div>
{/* 素材列表 */}
<div className="card">
{loading ? (
<div className="flex items-center justify-center py-12">
<LoadingSpinner size="large" />
</div>
) : materials.length === 0 ? (
<div className="text-center py-12">
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Upload className="w-8 h-8 text-gray-400" />
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2"></h3>
<p className="text-gray-600"></p>
</div>
) : (
<div className="divide-y divide-gray-100">
{materials.map((material) => (
<div
key={material.id}
className={`p-4 hover:bg-gray-50 transition-colors ${selectedMaterials.includes(material.id) ? 'bg-blue-50' : ''
}`}
>
<div className="flex items-center gap-4">
<input
type="checkbox"
checked={selectedMaterials.includes(material.id)}
onChange={() => handleMaterialSelect(material.id)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<h3 className="font-medium text-gray-900 truncate">{material.name}</h3>
</div>
<div className="flex items-center gap-6 text-sm text-gray-600">
<span>: {material.material_type}</span>
{material.file_size && (
<span>: {formatFileSize(material.file_size)}</span>
)}
{(() => {
const duration = getMaterialDuration(material);
return duration && (
<span>: {formatDuration(duration)}</span>
);
})()}
{(() => {
const dimensions = getMaterialDimensions(material);
return dimensions && (
<span>: {dimensions.width}×{dimensions.height}</span>
);
})()}
</div>
<div className="mt-1 text-xs text-gray-500 truncate">
{material.original_path}
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* 水印工具对话框 */}
{showWatermarkDialog && (
<WatermarkToolDialog
isOpen={showWatermarkDialog}
onClose={() => setShowWatermarkDialog(false)}
selectedMaterials={selectedMaterials.map(id => materials.find(m => m.id === id)!).filter(Boolean)}
/>
)}
</div>
);
};
export default WatermarkTool;