import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Camera, RefreshCw, Sparkles, Download, Trash2, Settings, Image as ImageIcon, Check, Share2, Laptop, ExternalLink, Smartphone, X, AlertCircle, Usb, BookOpen, DownloadCloud, Info, CheckCircle2, Eye, Palette, Clock, Sun, Shirt, CloudUpload, Link, RotateCcw, LogIn, Monitor, Smartphone as PortraitIcon, Flame, Printer, LayoutGrid, Video, ChevronDown, Box, FlipHorizontal, Heart, User, Star, ShieldCheck, Wand2, MonitorPlay, History, Maximize2, FolderOpen, Database, Activity } from 'lucide-react'; // Import Firebase import { initializeApp } from 'firebase/app'; import { getAuth, signInWithCustomToken, signInAnonymously, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, collection, addDoc, onSnapshot, query, deleteDoc, doc, serverTimestamp, setDoc } from 'firebase/firestore'; // --- KONFIGURASI GLOBAL --- const PRINT_WIDTH_PX = 1968; const PRINT_HEIGHT_PX = 2953; const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : { apiKey: "", authDomain: "", projectId: "", storageBucket: "", messagingSenderId: "", appId: "" }; const appId = typeof __app_id !== 'undefined' ? __app_id : 'wedding-photobooth-v26'; const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); // --- KATALOG GAYA STUDIO FULL BODY MASTERPIECE --- const AI_STYLES = [ { id: 'studio_casual_male_full', name: 'Studio Kasual Lelaki (Full Body)', prompt: 'Nano Banana AI Pro Masterpiece: Professional 8K Photo Studio Portrait. FULL BODY SHOT, head to toe visible. Subject wearing premium casual outfit: Smart navy blazer over a clean white t-shirt with elegant trousers and luxury sneakers. IDENTITY LOCK: 100% PRESERVE ORIGINAL FACE. AUTO-BEAUTY: Flawless smooth skin. Background: Clean minimalist studio grey textured. 500 DPI UHD.', color: 'from-slate-700 to-indigo-900', preview: 'https://images.unsplash.com/photo-1552374196-c4e7ffc6e126?auto=format&fit=crop&w=300&q=80' }, { id: 'studio_casual_female_full', name: 'Studio Kasual Wanita (Full Body)', prompt: 'Nano Banana AI Pro Masterpiece: Professional 8K Photo Studio Portrait. FULL BODY SHOT, head to toe visible. Subject wearing elegant casual outfit: Chic white silk blouse with premium wide-leg trousers or pastel linen set. IDENTITY LOCK: 100% PRESERVE ORIGINAL FACE. AUTO-BEAUTY: Porcelain skin, radiant natural glow. Background: Aesthetic soft beige studio. 500 DPI UHD.', color: 'from-rose-300 to-pink-500', preview: 'https://images.unsplash.com/photo-1529139513055-07f9127ef3b0?auto=format&fit=crop&w=300&q=80' } ]; const App = () => { // --- DETEKSI MODE --- const [isDisplayMode] = useState(() => { try { return window.location.search.includes('display=true'); } catch (e) { return false; } }); // --- STATE UTAMA --- const [user, setUser] = useState(null); const [devices, setDevices] = useState([]); const [selectedDevice, setSelectedDevice] = useState(''); const [stream, setStream] = useState(null); const [capturedImage, setCapturedImage] = useState(null); const [processedImages, setProcessedImages] = useState([]); const [selectedResultIdx, setSelectedResultIdx] = useState(0); const [gallery, setGallery] = useState([]); const [isCapturing, setIsCapturing] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [isFlashActive, setIsFlashActive] = useState(false); const [countdown, setCountdown] = useState(null); const [selectedStyle, setSelectedStyle] = useState(AI_STYLES[0]); const [showSettings, setShowSettings] = useState(false); const [uploadStatus, setUploadStatus] = useState(null); const [error, setError] = useState(null); const [isCameraReady, setIsCameraReady] = useState(false); const [isMirrored, setIsMirrored] = useState(true); const [orientation, setOrientation] = useState('portrait'); const [isoBoost, setIsoBoost] = useState(100); const [contrastBoost, setContrastBoost] = useState(100); const [previewActiveImage, setPreviewActiveImage] = useState(null); const [slideshowIndex, setSlideshowIndex] = useState(0); const broadcastChannel = useRef(null); const videoRef = useRef(null); const canvasRef = useRef(null); const slideshowInterval = useRef(null); const resetTimeout = useRef(null); const apiKey = ""; // --- UTILS: KOMPRESI & BASE64 --- const compressImage = async (base64, quality = 0.6) => { return new Promise((resolve) => { const img = new Image(); img.src = base64; img.onload = () => { const canvas = document.createElement('canvas'); const MAX_WIDTH = 800; const scale = MAX_WIDTH / img.width; canvas.width = MAX_WIDTH; canvas.height = img.height * scale; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); resolve(canvas.toDataURL('image/jpeg', quality)); }; }); }; // --- 🔥 LOGIKA SINKRONISASI ULTRA STABIL --- const sendSyncData = useCallback(() => { if (!isDisplayMode && broadcastChannel.current) { const payload = { capturedImage: capturedImage || null, processedImages: processedImages || [], selectedResultIdx: selectedResultIdx || 0, isProcessing: isProcessing || false, countdown: countdown || null, isFlashActive: isFlashActive || false, orientation: orientation || 'portrait', gallery: gallery || [], selectedStyle: selectedStyle?.name || '', timestamp: Date.now() }; broadcastChannel.current.postMessage({ type: 'SYNC_ALL', payload }); } }, [isDisplayMode, capturedImage, processedImages, selectedResultIdx, isProcessing, countdown, isFlashActive, orientation, gallery, selectedStyle]); // --- LOGIKA KAMERA --- const stopStream = useCallback(() => { if (stream) { stream.getTracks().forEach(track => track.stop()); setStream(null); } }, [stream]); const refreshDevices = async () => { if (isDisplayMode) return; try { const initial = await navigator.mediaDevices.getUserMedia({ video: true }); initial.getTracks().forEach(t => t.stop()); const allDevices = await navigator.mediaDevices.enumerateDevices(); const vids = allDevices.filter(d => d.kind === 'videoinput'); setDevices(vids); if (vids.length > 0 && !selectedDevice) { const pref = vids.find(d => d.label.toLowerCase().includes('canon') || d.label.toLowerCase().includes('eos')); setSelectedDevice(pref ? pref.deviceId : vids[0].deviceId); } } catch (err) { setError("Sila izinkan akses kamera."); } }; useEffect(() => { if (isDisplayMode || !selectedDevice) return; let active = true; const startCamera = async () => { setIsCameraReady(false); if (stream) stream.getTracks().forEach(t => t.stop()); try { const s = await navigator.mediaDevices.getUserMedia({ video: { deviceId: { exact: selectedDevice }, width: { ideal: 1920 }, height: { ideal: 1080 } } }); if (active) { setStream(s); setIsCameraReady(true); setError(null); } } catch (err) { if (active) setError("Kamera terputus."); } }; startCamera(); return () => { active = false; }; }, [selectedDevice, isDisplayMode]); useEffect(() => { if (!capturedImage && stream && videoRef.current) videoRef.current.srcObject = stream; }, [capturedImage, stream]); // --- BROADCAST CHANNEL EFFECT --- useEffect(() => { broadcastChannel.current = new BroadcastChannel('wedding_photobooth_sync_v13'); if (isDisplayMode) { broadcastChannel.current.postMessage({ type: 'DISPLAY_READY' }); broadcastChannel.current.onmessage = (event) => { const { type, payload } = event.data || {}; if (type === 'SYNC_ALL' && payload) { setCapturedImage(payload.capturedImage || null); setProcessedImages(payload.processedImages || []); setSelectedResultIdx(payload.selectedResultIdx || 0); setIsProcessing(payload.isProcessing || false); setCountdown(payload.countdown || null); setIsFlashActive(payload.isFlashActive || false); setOrientation(payload.orientation || 'portrait'); setGallery(payload.gallery || []); } }; } else { broadcastChannel.current.onmessage = (event) => { if (event.data?.type === 'DISPLAY_READY') sendSyncData(); }; refreshDevices(); } return () => broadcastChannel.current?.close(); }, [isDisplayMode, sendSyncData]); // Auto-Sync Effect useEffect(() => { if (!isDisplayMode) sendSyncData(); }, [capturedImage, processedImages, isProcessing, countdown, isFlashActive, orientation, gallery, sendSyncData, isDisplayMode]); // --- 🔥 PHOTOBOOTH MALL EXPERIENCE (AUTO SLIDESHOW & RESET) --- useEffect(() => { if (isDisplayMode && processedImages && processedImages.length > 0) { setSlideshowIndex(0); slideshowInterval.current = setInterval(() => { setSlideshowIndex(prev => (prev + 1) % processedImages.length); }, 2500); resetTimeout.current = setTimeout(() => { setCapturedImage(null); setProcessedImages([]); setSlideshowIndex(0); }, 15000); } return () => { if (slideshowInterval.current) clearInterval(slideshowInterval.current); if (resetTimeout.current) clearTimeout(resetTimeout.current); }; }, [processedImages, isDisplayMode]); // --- FIREBASE AUTH --- useEffect(() => { const initAuth = async () => { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (err) { console.error("Auth error", err); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, setUser); return () => unsubscribe(); }, []); useEffect(() => { if (!user) return; const photosCol = collection(db, 'artifacts', appId, 'public', 'data', 'wedding_photos'); const unsubscribe = onSnapshot(photosCol, (snap) => { const docs = snap.docs.map(d => ({ id: d.id, ...d.data() })); const sorted = docs.sort((a, b) => (b.createdAt?.seconds || 0) - (a.createdAt?.seconds || 0)); setGallery(sorted); }); return () => unsubscribe(); }, [user]); // --- HANDLERS --- const openDisplayWindow = () => { const url = new URL(window.location.href); url.searchParams.set('display', 'true'); window.open(url.toString(), '_blank', 'width=1280,height=720,menubar=no,status=no,toolbar=no'); }; const handlePrint = (imgSrc) => { if (!imgSrc) return; const pWin = window.open('', '_blank'); if (pWin) { pWin.document.write(`Print`); pWin.document.close(); } }; const triggerShutter = () => { setIsFlashActive(true); const video = videoRef.current; if (video && canvasRef.current) { const targetW = orientation === 'landscape' ? PRINT_HEIGHT_PX : PRINT_WIDTH_PX; const targetH = orientation === 'landscape' ? PRINT_WIDTH_PX : PRINT_HEIGHT_PX; canvasRef.current.width = targetW; canvasRef.current.height = targetH; const ctx = canvasRef.current.getContext('2d'); ctx.filter = `brightness(${isoBoost}%) contrast(${contrastBoost}%)`; const sW = video.videoWidth; const sH = video.videoHeight; const tA = targetW / targetH; const sA = sW / sH; let dW, dH, oX, oY; if (sA > tA) { dW = sH * tA; dH = sH; oX = (sW - dW) / 2; oY = 0; } else { dW = sW; dH = sW / tA; oX = 0; oY = (sH - dH) / 2; } if (isMirrored) { ctx.save(); ctx.translate(targetW, 0); ctx.scale(-1, 1); ctx.drawImage(video, oX, oY, dW, dH, 0, 0, targetW, targetH); ctx.restore(); } else { ctx.drawImage(video, oX, oY, dW, dH, 0, 0, targetW, targetH); } setCapturedImage(canvasRef.current.toDataURL('image/png', 1.0)); } setCountdown(null); setIsCapturing(false); setTimeout(() => setIsFlashActive(false), 100); }; const takePhoto = () => { if (isCapturing || !isCameraReady) return; let count = 3; setCountdown(count); setIsCapturing(true); const timer = setInterval(() => { count--; if (count === 0) { clearInterval(timer); triggerShutter(); } else setCountdown(count); }, 800); }; // --- 🔥 BAGIAN 3: UPDATE applyAIStep() DENGAN BASE64 MURNI --- const applyAIStep = async () => { if (!capturedImage) return; setIsProcessing(true); try { const results = await Promise.all([1, 2, 3, 4].map(async (v) => { const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: `${selectedStyle.prompt} High Fidelity Variation ${v}. FULL BODY MASTERPIECE.` }, { inlineData: { mimeType: "image/png", data: capturedImage.split(',')[1] } }] }], generationConfig: { responseModalities: ['TEXT', 'IMAGE'], temperature: 0.05 } }) }); const r = await response.json(); // Langsung ambil base64 dari response Gemini (lebih cepat & stabil) const base64Data = r.candidates?.[0]?.content?.parts?.find(p => p.inlineData)?.inlineData?.data; return base64Data ? `data:image/png;base64,${base64Data}` : null; })); const validResults = results.filter(r => r !== null); setProcessedImages(validResults); setSelectedResultIdx(0); // Force sync setelah React selesai update setTimeout(() => sendSyncData(), 300); } catch (err) { setError("AI sedang sibuk. Sila cuba lagi."); } finally { setIsProcessing(false); } }; const handleSaveAndAutomate = async () => { const finalData = (processedImages && processedImages.length > 0) ? processedImages[selectedResultIdx] : capturedImage; if (!finalData) return; setUploadStatus('uploading'); try { const photosCol = collection(db, 'artifacts', appId, 'public', 'data', 'wedding_photos'); await addDoc(photosCol, { image: await compressImage(finalData, 0.7), createdAt: serverTimestamp(), style: selectedStyle.name }); } catch (e) { console.error(e); } const dl = document.createElement('a'); dl.href = finalData; dl.download = `Photo-8K-FullBody.png`; dl.click(); setTimeout(() => { setUploadStatus('success'); setTimeout(() => { setUploadStatus(null); setCapturedImage(null); setProcessedImages([]); }, 1000); }, 1500); }; // --- RENDER MOD DISPLAY (TETAMU) --- if (isDisplayMode) { return (

The Wedding of

Tian & Lisa

Masterpiece Sync
{!capturedImage && (!processedImages || processedImages.length === 0) ? (

BERPOSELAH

) : (
{isProcessing ? (

MENJANA KEAJAIBAN

) : (
{processedImages && processedImages.length > 0 ? (
Masterpiece
Variasi {slideshowIndex + 1}
) : (
Original
)}
)}
)}
{gallery.map((item, i) => ( Session ))}
{countdown && (
{countdown}
)} {isFlashActive &&
}
); } // --- RENDER MOD OPERATOR --- return (
{error && (
{error}
)}
{!capturedImage ? ( <>
{/* FULL PREVIEW MODAL OPERATOR */} {previewActiveImage && (
setPreviewActiveImage(null)}>
e.stopPropagation()}> Full
)}