Provide users with advanced options for video thumbnail generation
Adds FFmpeg/ImageMagick integration for dynamic thumbnail creation with API endpoints. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 50814a1e-92e4-4968-856f-7bc7eedf5e8f Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/50814a1e-92e4-4968-856f-7bc7eedf5e8f/L923Cjb
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Download, RefreshCw, Trash2 } from "lucide-react";
|
||||
import { type Video } from "@shared/schema";
|
||||
|
||||
interface ThumbnailGeneratorProps {
|
||||
video: Video;
|
||||
onThumbnailGenerated?: (thumbnailUrl: string) => void;
|
||||
}
|
||||
|
||||
export default function ThumbnailGenerator({ video, onThumbnailGenerated }: ThumbnailGeneratorProps) {
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [customTime, setCustomTime] = useState("5");
|
||||
const [generatedThumbnails, setGeneratedThumbnails] = useState<Array<{
|
||||
timestamp: string;
|
||||
url: string;
|
||||
path: string;
|
||||
}>>([]);
|
||||
|
||||
const generateThumbnail = async (timestamp: string) => {
|
||||
setIsGenerating(true);
|
||||
try {
|
||||
const response = await fetch(`/api/thumbnails/${video.id}/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
timestamps: [timestamp]
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.thumbnails && data.thumbnails.length > 0) {
|
||||
const newThumbnail = data.thumbnails[0];
|
||||
setGeneratedThumbnails(prev => [...prev, newThumbnail]);
|
||||
|
||||
if (onThumbnailGenerated) {
|
||||
onThumbnailGenerated(newThumbnail.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating thumbnail:', error);
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generateMultipleThumbnails = async () => {
|
||||
setIsGenerating(true);
|
||||
try {
|
||||
const timestamps = ["5", "15", "30", "60", "90"];
|
||||
const response = await fetch(`/api/thumbnails/${video.id}/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
timestamps
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setGeneratedThumbnails(data.thumbnails || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating thumbnails:', error);
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadExistingThumbnails = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/thumbnails/${video.id}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setGeneratedThumbnails(data.thumbnails || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading thumbnails:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useState(() => {
|
||||
loadExistingThumbnails();
|
||||
}, [video.id]);
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-4xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<RefreshCw className="w-5 h-5" />
|
||||
Ustvarjanje Thumbnail Slik
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Generirajte thumbnail slike iz različnih trenutkov videa "{video.title}"
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Quick generate buttons */}
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">Hitro generiranje</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={generateMultipleThumbnails}
|
||||
disabled={isGenerating}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{isGenerating ? "Generiranje..." : "Generiraj 5 slik (5s, 15s, 30s, 60s, 90s)"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom time input */}
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">Določi čas</Label>
|
||||
<div className="flex gap-2 max-w-sm">
|
||||
<Input
|
||||
type="number"
|
||||
value={customTime}
|
||||
onChange={(e) => setCustomTime(e.target.value)}
|
||||
placeholder="Sekunde (npr. 30)"
|
||||
min="0"
|
||||
max={video.duration || 300}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => generateThumbnail(customTime)}
|
||||
disabled={isGenerating}
|
||||
variant="outline"
|
||||
>
|
||||
Generiraj
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Vnesite čas v sekundah (0 - {video.duration || 300}s)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Generated thumbnails grid */}
|
||||
{generatedThumbnails.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">Generirane slike</Label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{generatedThumbnails.map((thumbnail, index) => (
|
||||
<div key={index} className="relative group">
|
||||
<div className="aspect-video bg-gray-100 rounded-lg overflow-hidden">
|
||||
<img
|
||||
src={thumbnail.url}
|
||||
alt={`Thumbnail at ${thumbnail.timestamp}s`}
|
||||
className="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-2 left-2 bg-black/80 px-2 py-1 rounded text-xs text-white">
|
||||
{thumbnail.timestamp}s
|
||||
</div>
|
||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => {
|
||||
const link = document.createElement('a');
|
||||
link.href = thumbnail.url;
|
||||
link.download = `thumbnail-${video.id}-${thumbnail.timestamp}s.jpg`;
|
||||
link.click();
|
||||
}}
|
||||
>
|
||||
<Download className="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => {
|
||||
if (onThumbnailGenerated) {
|
||||
onThumbnailGenerated(thumbnail.url);
|
||||
}
|
||||
}}
|
||||
>
|
||||
✓
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="text-sm text-muted-foreground space-y-2 border-l-4 border-blue-500 pl-4">
|
||||
<p><strong>Navodila:</strong></p>
|
||||
<ul className="space-y-1 list-disc list-inside ml-2">
|
||||
<li>Kliknite "Generiraj 5 slik" za hitro ustvarjanje slik iz različnih trenutkov</li>
|
||||
<li>Uporabite "Določi čas" za ustvarjanje slike iz določene sekunde</li>
|
||||
<li>Kliknite ✓ da nastavite sliko kot glavno thumbnail</li>
|
||||
<li>Kliknite ⬇ da prenesete sliko na svoj računalnik</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user