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:
sebastjanartic
2025-08-04 19:51:11 +00:00
parent 1321038609
commit 60cf545f79
5 changed files with 492 additions and 58 deletions
@@ -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>
);
}