182 lines
5.7 KiB
TypeScript
182 lines
5.7 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import { FileSelector } from './components/FileSelector';
|
|
import './styles/index.css';
|
|
|
|
interface FileState {
|
|
path: string;
|
|
name: string;
|
|
}
|
|
|
|
function App() {
|
|
const [videoFile, setVideoFile] = useState<FileState | null>(null);
|
|
const [subtitleFile, setSubtitleFile] = useState<FileState | null>(null);
|
|
const [progress, setProgress] = useState<number>(0);
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [addBlackBar, setAddBlackBar] = useState(true);
|
|
const [resizeTo720p, setResizeTo720p] = useState(true);
|
|
|
|
useEffect(() => {
|
|
invoke('check_dependencies')
|
|
.catch(err => {
|
|
setError(err as string);
|
|
console.error('Dependency check failed:', err);
|
|
});
|
|
}, []);
|
|
|
|
const handleVideoSelect = (path: string) => {
|
|
const name = path.split('/').pop() || '';
|
|
setVideoFile({ path, name });
|
|
};
|
|
|
|
const handleSubtitleSelect = (path: string) => {
|
|
const name = path.split('/').pop() || '';
|
|
setSubtitleFile({ path, name });
|
|
};
|
|
|
|
useEffect(() => {
|
|
let intervalId: number | undefined;
|
|
|
|
if (isProcessing) {
|
|
// Poll for progress every 100ms while processing
|
|
intervalId = window.setInterval(async () => {
|
|
try {
|
|
const currentProgress = await invoke<number>('get_progress');
|
|
console.log('Progress update:', currentProgress);
|
|
setProgress(currentProgress);
|
|
} catch (error) {
|
|
console.error('Failed to get progress:', error);
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
return () => {
|
|
if (intervalId !== undefined) {
|
|
window.clearInterval(intervalId);
|
|
}
|
|
};
|
|
}, [isProcessing]);
|
|
|
|
const handleMerge = async () => {
|
|
if (!videoFile || !subtitleFile) return;
|
|
|
|
setIsProcessing(true);
|
|
setProgress(0);
|
|
|
|
try {
|
|
console.log('Starting merge with args:', {
|
|
videoPath: videoFile.path,
|
|
subtitlePath: subtitleFile.path,
|
|
addBlackBar,
|
|
resizeTo720p
|
|
});
|
|
|
|
const result = await invoke<string>('merge_video_subtitle', {
|
|
args: {
|
|
video_path: videoFile.path,
|
|
subtitle_path: subtitleFile.path,
|
|
add_black_bar: addBlackBar,
|
|
resize_to_720p: resizeTo720p
|
|
}
|
|
});
|
|
|
|
console.log('Merge completed:', result);
|
|
setProgress(100);
|
|
} catch (error) {
|
|
console.error('Error merging files:', error);
|
|
setError(error as string);
|
|
} finally {
|
|
setIsProcessing(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-100 py-8 px-4">
|
|
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-md p-6">
|
|
<h1 className="text-2xl font-bold text-center mb-8">Video Subtitle Merger <span className="text-blue-500">2.0</span></h1>
|
|
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex-1 mr-4">
|
|
<p className="text-sm text-gray-600 mb-2">Video file:</p>
|
|
<p className="text-gray-800">{videoFile?.name || 'No file selected'}</p>
|
|
</div>
|
|
<FileSelector
|
|
label="Select Video"
|
|
onFileSelect={handleVideoSelect}
|
|
accept={['mp4', 'avi', 'mov', 'mkv']}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex-1 mr-4">
|
|
<p className="text-sm text-gray-600 mb-2">Subtitle file:</p>
|
|
<p className="text-gray-800">{subtitleFile?.name || 'No file selected'}</p>
|
|
</div>
|
|
<FileSelector
|
|
label="Select Subtitle"
|
|
onFileSelect={handleSubtitleSelect}
|
|
accept={['srt', 'ass']}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<label className="flex items-center space-x-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={addBlackBar}
|
|
onChange={(e) => setAddBlackBar(e.target.checked)}
|
|
className="input-checkbox"
|
|
/>
|
|
<span>Add black bar for subtitles</span>
|
|
</label>
|
|
|
|
<label className="flex items-center space-x-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={resizeTo720p}
|
|
onChange={(e) => setResizeTo720p(e.target.checked)}
|
|
className="input-checkbox"
|
|
/>
|
|
<span>Resize to 720p resolution</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div className="h-2.5 bg-gray-200 rounded-full">
|
|
<div
|
|
className="h-2.5 bg-blue-600 rounded-full transition-all duration-300"
|
|
style={{ width: `${progress}%` }}
|
|
/>
|
|
</div>
|
|
<p className="text-center text-sm text-gray-600">
|
|
{isProcessing ? `${Math.round(progress)}%` : ''}
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4">
|
|
<strong className="font-bold">Error: </strong>
|
|
<span className="block sm:inline">{error}</span>
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
onClick={handleMerge}
|
|
disabled={!videoFile || !subtitleFile || isProcessing}
|
|
className={`w-full btn ${
|
|
!videoFile || !subtitleFile || isProcessing
|
|
? 'bg-gray-400 cursor-not-allowed'
|
|
: 'btn-primary'
|
|
}`}
|
|
>
|
|
{isProcessing ? 'Processing...' : 'Merge Video and Subtitle'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|