import { useState, useRef } from "react"; import { useToast } from "@/hooks/use-toast"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Slider } from "@/components/ui/slider"; import { UploadCloud, Video, Loader2, Menu, Check, Sparkles, Mic, Volume2, Music, Play, Pause, Send, Camera, ImagePlus } from "lucide-react"; import { MediaThumbnail } from "@/components/media-thumbnail"; import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import Sidebar from "@/components/sidebar"; import { Checkbox } from "@/components/ui/checkbox"; import { DateTimePicker } from "@/components/datetime-picker"; import { useQuery, useMutation } from "@tanstack/react-query"; import { apiRequest, queryClient } from "@/lib/queryClient"; import type { Media, SocialPage } from "@shared/schema"; interface AudioTrack { id: string; title: string; fileName: string; url: string; duration: number; } function stripForTTS(text: string) { return text.replace(/#\w+/g, '').replace(/[\uD800-\uDFFF]/g, '').replace(/[\u2600-\u27BF]/g, '').replace(/\s+/g, ' ').trim(); } export default function MobileRemotionVideoPage() { const [images, setImages] = useState([]); const [selectedLibraryImages, setSelectedLibraryImages] = useState([]); const [overlayText, setOverlayText] = useState(""); const [productInfo, setProductInfo] = useState(""); const [generatedVariants, setGeneratedVariants] = useState([]); const [ttsEnabled, setTtsEnabled] = useState(true); const [musicFile, setMusicFile] = useState(null); const [selectedTrack, setSelectedTrack] = useState(null); const [musicVolume, setMusicVolume] = useState(0.3); const [isPlaying, setIsPlaying] = useState(null); const [isRendering, setIsRendering] = useState(false); const [videoUrl, setVideoUrl] = useState(null); const [selectedPageIds, setSelectedPageIds] = useState([]); const [scheduledDate, setScheduledDate] = useState(undefined); const [publishDescription, setPublishDescription] = useState(""); const [isPublishing, setIsPublishing] = useState(false); const audioRef = useRef(null); const cameraInputRef = useRef(null); const galleryInputRef = useRef(null); const { toast } = useToast(); const { data: allMedia = [] } = useQuery({ queryKey: ['/api/media'] }); const { data: audioTracks = [], isLoading: tracksLoading } = useQuery({ queryKey: ['/api/audio-tracks'] }); const { data: socialPages = [] } = useQuery({ queryKey: ['/api/pages'] }); const facebookPages = socialPages.filter(p => p.platform === 'facebook'); const imageMediaList = allMedia.filter(m => m.type === 'image').slice(0, 20); const generateTextMutation = useMutation({ mutationFn: async (text: string) => (await apiRequest('POST', '/api/reels/generate-text', { productInfo: text })).json(), onSuccess: (data: any) => { setGeneratedVariants(data.variants || []); toast({ title: "Textes générés" }); }, onError: () => toast({ title: "Erreur IA", variant: "destructive" }), }); const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { const filesArray = Array.from(e.target.files); const remaining = 4 - selectedLibraryImages.length - images.length; // Corriger les noms de fichiers pour les fichiers venant de la galerie mobile const processedFiles = filesArray.slice(0, remaining).map(file => { if (!file.name || file.name === 'blob' || file.name === '' || file.name === 'image') { const ext = file.type.split('/')[1] || 'jpg'; return new File([file], `gallery-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.${ext}`, { type: file.type }); } return file; }); setImages(prev => [...prev, ...processedFiles].slice(0, 4 - selectedLibraryImages.length)); } // Reset input pour permettre de resélectionner les mêmes fichiers e.target.value = ''; }; const handleCameraCapture = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const remaining = 4 - selectedLibraryImages.length - images.length; if (remaining <= 0) { toast({ title: "4 images max.", variant: "destructive" }); e.target.value = ''; return; } let fileToAdd = file; if (!file.name || file.name === 'blob' || file.name === '') { const ext = file.type.split('/')[1] || 'jpg'; fileToAdd = new File([file], `camera-${Date.now()}.${ext}`, { type: file.type }); } setImages(prev => [...prev, fileToAdd].slice(0, 4 - selectedLibraryImages.length)); e.target.value = ''; }; const toggleLibraryImage = (media: Media) => { setSelectedLibraryImages(prev => { if (prev.find(m => m.id === media.id)) return prev.filter(m => m.id !== media.id); if (images.length + prev.length >= 4) { toast({ title: "4 images max.", variant: "destructive" }); return prev; } return [...prev, media]; }); }; const totalSelected = images.length + selectedLibraryImages.length; const handleTtsPreview = async () => { const ttsText = stripForTTS(overlayText); if (!ttsText) return; try { const r = await apiRequest('POST', '/api/reels/tts-preview', { text: ttsText }); const data = await r.json(); if (data.success && data.audioBase64) new window.Audio(`data:audio/mp3;base64,${data.audioBase64}`).play(); } catch { toast({ title: "Erreur prévisualisation voix", variant: "destructive" }); } }; const togglePlayPreview = (track: AudioTrack) => { if (!audioRef.current) return; if (isPlaying === track.id) { audioRef.current.pause(); setIsPlaying(null); } else { audioRef.current.src = track.url; audioRef.current.play(); setIsPlaying(track.id); } }; const handleGenerate = async () => { if (totalSelected < 3) { toast({ title: "Minimum 3 images", variant: "destructive" }); return; } setIsRendering(true); setVideoUrl(null); try { const formData = new FormData(); images.forEach(img => formData.append("images", img)); selectedLibraryImages.forEach(m => formData.append("existingImageUrls", m.originalUrl)); if (overlayText) formData.append("overlayText", overlayText); if (selectedPageIds[0]) formData.append("selectedPageId", selectedPageIds[0]); if (musicFile) { formData.append("music", musicFile); formData.append("musicVolume", String(musicVolume)); } else if (selectedTrack) { formData.append("musicTrackUrl", selectedTrack.url); formData.append("musicVolume", String(musicVolume)); } const response = await fetch("/api/remotion/render", { method: "POST", body: formData }); if (!response.ok) { const e = await response.json().catch(() => ({})); throw new Error(e.error || "Erreur"); } const data = await response.json(); if (data.jobId) { toast({ title: "Rendu démarré", description: "Veuillez patienter (~1-2 min)..." }); const checkStatus = async () => { try { const res = await fetch(`/api/remotion/render/status/${data.jobId}`); if (!res.ok) throw new Error("Erreur de suivi du rendu"); const job = await res.json(); if (job.status === 'done') { setVideoUrl(job.url); if (overlayText && !publishDescription) setPublishDescription(overlayText); toast({ title: "Vidéo générée !" }); setIsRendering(false); } else if (job.status === 'error') { toast({ title: "Erreur de rendu", description: job.error || "Échec du rendu", variant: "destructive" }); setIsRendering(false); } else { setTimeout(checkStatus, 3000); } } catch (e: any) { toast({ title: "Erreur de connexion", description: e.message, variant: "destructive" }); setIsRendering(false); } }; setTimeout(checkStatus, 3000); } else { setVideoUrl(data.url); if (overlayText && !publishDescription) setPublishDescription(overlayText); toast({ title: "Vidéo générée !" }); setIsRendering(false); } } catch (err: any) { toast({ title: "Erreur", description: err.message, variant: "destructive" }); setIsRendering(false); } }; const handlePublish = async () => { if (!videoUrl) return; if (selectedPageIds.length === 0) { toast({ title: "Sélectionnez au moins une page.", variant: "destructive" }); return; } setIsPublishing(true); try { const response = await apiRequest('POST', '/api/remotion/publish', { videoUrl, pageIds: selectedPageIds, scheduledFor: scheduledDate?.toISOString(), description: publishDescription || undefined, }); const data = await response.json(); if (!data.success) throw new Error(data.results?.find((r: any) => r.error)?.error || "Erreur"); toast({ title: scheduledDate ? "Publication planifiée !" : "Vidéo publiée !", description: scheduledDate ? `Sera publiée le ${scheduledDate.toLocaleString('fr-FR')}` : "Publiée sur Facebook.", }); await queryClient.invalidateQueries({ queryKey: ['/api/scheduled-posts'], refetchType: 'all' }); } catch (err: any) { toast({ title: "Erreur de publication", description: err.message, variant: "destructive" }); } finally { setIsPublishing(false); } }; return (