/ App.tsx
App.tsx
  1  
  2  import React, { useState, useEffect, useRef, useCallback } from 'react';
  3  import { Upload, Save, RefreshCw, Eraser, Info, Shield, CheckCircle, Globe, Play, Pause, Video, MonitorPlay, FileVideo, FileImage, VolumeX, ShieldPlus } from 'lucide-react';
  4  import { SUPPORTED_LANGUAGES, TRANSLATIONS } from './constants';
  5  import { LanguageCode, MediaState, FilterState, BlurRegion } from './types';
  6  import { renderToCanvas } from './utils/imageProcessing';
  7  import { Button } from './components/ui/Button';
  8  import { Slider } from './components/ui/Slider';
  9  
 10  function App() {
 11    // --- State ---
 12    const [lang, setLang] = useState<LanguageCode>('en');
 13    const t = TRANSLATIONS[lang];
 14    const isRTL = lang === 'ar' || lang === 'fa';
 15  
 16    const [mediaState, setMediaState] = useState<MediaState>({
 17      type: 'image',
 18      file: null,
 19      source: null,
 20      url: null,
 21      width: 0,
 22      height: 0,
 23      duration: 0,
 24      loaded: false,
 25      isPlaying: false,
 26      isRecording: false
 27    });
 28  
 29    const [filters, setFilters] = useState<FilterState>({
 30      grayscale: 0,
 31      noise: 0,
 32      scanlines: 0,
 33      rgbShift: 0,
 34      glitchIntensity: 0,
 35      blurRegions: [],
 36    });
 37  
 38    const [blurMode, setBlurMode] = useState(false);
 39    const [audioEnabled, setAudioEnabled] = useState(false); // Audio OFF by default (secure)
 40    const [isDragging, setIsDragging] = useState(false);
 41    const [dragStart, setDragStart] = useState<{ x: number, y: number } | null>(null);
 42    const [dragCurrent, setDragCurrent] = useState<{ x: number, y: number } | null>(null);
 43    const [showHelp, setShowHelp] = useState(false);
 44    const [isLangMenuOpen, setIsLangMenuOpen] = useState(false);
 45    const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
 46    const [showInstallButton, setShowInstallButton] = useState(false);
 47  
 48    // --- Refs ---
 49    const canvasRef = useRef<HTMLCanvasElement>(null);
 50    const containerRef = useRef<HTMLDivElement>(null);
 51    const fileInputRef = useRef<HTMLInputElement>(null);
 52    const videoRef = useRef<HTMLVideoElement | null>(null);
 53    const requestRef = useRef<number | null>(null);
 54    const mediaRecorderRef = useRef<MediaRecorder | null>(null);
 55    const recordedChunksRef = useRef<Blob[]>([]);
 56  
 57    // --- Animation Loop for Video ---
 58    useEffect(() => {
 59      const animateFrame = () => {
 60        if (mediaState.type === 'video' && mediaState.source && canvasRef.current) {
 61          // Render if playing, recording, OR if we just want to update the filter preview while paused
 62          if (mediaState.isPlaying || mediaState.isRecording) {
 63            renderToCanvas(canvasRef.current, mediaState.source as HTMLVideoElement, filters);
 64            requestRef.current = requestAnimationFrame(animateFrame);
 65          } else {
 66             // Even if paused, render one frame to show filter updates
 67             // But we don't loop.
 68             renderToCanvas(canvasRef.current, mediaState.source as HTMLVideoElement, filters);
 69          }
 70        }
 71      };
 72  
 73      if (mediaState.isPlaying || mediaState.isRecording) {
 74        requestRef.current = requestAnimationFrame(animateFrame);
 75      } else {
 76        // Trigger single render when filters change while paused
 77        animateFrame();
 78      }
 79  
 80      return () => {
 81        if (requestRef.current) cancelAnimationFrame(requestRef.current);
 82      };
 83    }, [mediaState.type, mediaState.source, mediaState.isPlaying, mediaState.isRecording, filters]);
 84  
 85    // --- Render Image to Canvas ---
 86    useEffect(() => {
 87      if (mediaState.type === 'image' && mediaState.source && mediaState.loaded && canvasRef.current) {
 88        renderToCanvas(canvasRef.current, mediaState.source as ImageBitmap, filters);
 89      }
 90    }, [mediaState.type, mediaState.source, mediaState.loaded, filters]);
 91  
 92    // --- PWA Install Prompt ---
 93    useEffect(() => {
 94      // Check if already running as PWA
 95      const isStandalone = window.matchMedia('(display-mode: standalone)').matches ||
 96                          (window.navigator as any).standalone ||
 97                          document.referrer.includes('android-app://');
 98  
 99      if (isStandalone) {
100        setShowInstallButton(false);
101        return;
102      }
103  
104      const handleBeforeInstallPrompt = (e: Event) => {
105        e.preventDefault();
106        setDeferredPrompt(e);
107        setShowInstallButton(true);
108      };
109  
110      window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
111  
112      return () => {
113        window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
114      };
115    }, []);
116  
117    const handleInstallClick = async () => {
118      if (!deferredPrompt) return;
119  
120      deferredPrompt.prompt();
121      const { outcome } = await deferredPrompt.userChoice;
122  
123      if (outcome === 'accepted') {
124        setShowInstallButton(false);
125      }
126  
127      setDeferredPrompt(null);
128    };
129  
130    // --- Handlers ---
131  
132    const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
133      if (e.target.files && e.target.files[0]) {
134        const file = e.target.files[0];
135        const isVideo = file.type.startsWith('video/');
136        
137        // Clean up old URL
138        if (mediaState.url) URL.revokeObjectURL(mediaState.url);
139  
140        if (isVideo) {
141          const url = URL.createObjectURL(file);
142          // Create hidden video element
143          const vid = document.createElement('video');
144          vid.src = url;
145          vid.muted = true; // Mute for processing
146          vid.loop = true;
147          vid.playsInline = true;
148          vid.crossOrigin = "anonymous";
149          
150          // Wait for metadata to load
151          vid.onloadedmetadata = () => {
152            setMediaState({
153              type: 'video',
154              file,
155              source: vid,
156              url,
157              width: vid.videoWidth,
158              height: vid.videoHeight,
159              duration: vid.duration,
160              loaded: true,
161              isPlaying: true, // Auto-play on load so user sees it works
162              isRecording: false
163            });
164            // Reset filters for new file
165            setFilters({
166              grayscale: 0,
167              noise: 0,
168              scanlines: 0,
169              rgbShift: 0,
170              glitchIntensity: 0,
171              blurRegions: [],
172            });
173            setBlurMode(false);
174            videoRef.current = vid;
175            vid.play();
176          };
177        } else {
178          // Image
179          try {
180            const bitmap = await createImageBitmap(file);
181            setMediaState({
182              type: 'image',
183              file,
184              source: bitmap,
185              url: null,
186              width: bitmap.width,
187              height: bitmap.height,
188              duration: 0,
189              loaded: true,
190              isPlaying: false,
191              isRecording: false
192            });
193             // Reset filters for new file
194             setFilters({
195              grayscale: 0,
196              noise: 0,
197              scanlines: 0,
198              rgbShift: 0,
199              glitchIntensity: 0,
200              blurRegions: [],
201            });
202            setBlurMode(false);
203            videoRef.current = null;
204          } catch (err) {
205            console.error("Failed to load image", err);
206            alert("Error loading image");
207          }
208        }
209      }
210    };
211  
212    const generateRandomFilename = (ext: string) => {
213      const nameLength = Math.floor(Math.random() * 9) + 8;
214      const randomName = Array.from({ length: nameLength }, () => 
215        'abcdefghijklmnopqrstuvwxyz0123456789'[Math.floor(Math.random() * 36)]
216      ).join('');
217      // Purely random filename for privacy, no project prefix
218      return `${randomName}.${ext}`;
219    };
220  
221    const handleSave = async () => {
222      if (mediaState.type === 'image') {
223        // Save Image
224        const canvas = canvasRef.current;
225        if (!canvas) return;
226  
227        canvas.toBlob((blob) => {
228          if (blob) {
229            const url = URL.createObjectURL(blob);
230            const a = document.createElement('a');
231            a.href = url;
232            a.download = generateRandomFilename('jpg');
233            document.body.appendChild(a);
234            a.click();
235            document.body.removeChild(a);
236            URL.revokeObjectURL(url);
237          }
238        }, 'image/jpeg', 0.9);
239  
240      } else if (mediaState.type === 'video') {
241        // If already recording, this button acts as stop? 
242        // The UI logic below separates "Record" from "Save" usually, 
243        // but let's make the main action button handle the recording flow.
244        
245        if (mediaState.isRecording) {
246           // Stop recording
247           const vid = mediaState.source as HTMLVideoElement;
248           if (vid && mediaRecorderRef.current) {
249             mediaRecorderRef.current.stop();
250           }
251           return;
252        }
253  
254        // Start Recording
255        const canvas = canvasRef.current;
256        const vid = mediaState.source as HTMLVideoElement;
257        if (!canvas || !vid) return;
258  
259        // 1. Reset Video
260        vid.pause();
261        vid.currentTime = 0;
262  
263        // 2. Start Playback & Recording
264        const canvasStream = canvas.captureStream(30); // 30 FPS recording
265  
266        // 3. Conditionally add audio
267        let recordingStream: MediaStream;
268        if (audioEnabled) {
269          // Capture audio from video element
270          try {
271            const videoStream = (vid as any).captureStream();
272            const audioTracks = videoStream.getAudioTracks();
273  
274            if (audioTracks.length > 0) {
275              // Combine video from canvas with audio from video
276              recordingStream = new MediaStream([
277                ...canvasStream.getVideoTracks(),
278                ...audioTracks
279              ]);
280            } else {
281              // No audio track in video, use canvas only
282              recordingStream = canvasStream;
283            }
284          } catch (e) {
285            // captureStream not supported, fallback to no audio
286            console.warn('Audio capture not supported', e);
287            recordingStream = canvasStream;
288          }
289        } else {
290          // Audio disabled (secure mode)
291          recordingStream = canvasStream;
292        }
293  
294        const mediaRecorder = new MediaRecorder(recordingStream, {
295          mimeType: audioEnabled ? 'video/webm;codecs=vp9,opus' : 'video/webm;codecs=vp9'
296        });
297        
298        mediaRecorderRef.current = mediaRecorder;
299        recordedChunksRef.current = [];
300  
301        mediaRecorder.ondataavailable = (e) => {
302          if (e.data.size > 0) recordedChunksRef.current.push(e.data);
303        };
304  
305        mediaRecorder.onstop = () => {
306          const blob = new Blob(recordedChunksRef.current, { type: 'video/webm' });
307          const url = URL.createObjectURL(blob);
308          const a = document.createElement('a');
309          a.href = url;
310          a.download = generateRandomFilename('webm');
311          document.body.appendChild(a);
312          a.click();
313          document.body.removeChild(a);
314          URL.revokeObjectURL(url);
315          
316          // Restore state
317          setMediaState(prev => ({ ...prev, isRecording: false, isPlaying: false }));
318          vid.pause();
319          vid.currentTime = 0;
320          vid.loop = true;
321        };
322  
323        // Start
324        setMediaState(prev => ({ ...prev, isRecording: true, isPlaying: true }));
325        mediaRecorder.start();
326        vid.play();
327        vid.loop = false; // Play once through for recording
328        vid.onended = () => {
329           if (mediaRecorder.state === "recording") {
330             mediaRecorder.stop();
331           }
332        };
333      }
334    };
335  
336    const handleVideoToggle = () => {
337      const vid = mediaState.source as HTMLVideoElement;
338      if (!vid) return;
339  
340      if (mediaState.isPlaying) {
341        vid.pause();
342        setMediaState(prev => ({ ...prev, isPlaying: false }));
343      } else {
344        vid.play();
345        setMediaState(prev => ({ ...prev, isPlaying: true }));
346      }
347    };
348  
349    const handleReset = () => {
350      setFilters({ 
351        grayscale: 0, 
352        noise: 0, 
353        scanlines: 0, 
354        rgbShift: 0, 
355        glitchIntensity: 0, 
356        blurRegions: [] 
357      });
358    };
359  
360    // --- Canvas Interaction ---
361  
362    const getCanvasCoords = (e: React.MouseEvent | React.TouchEvent) => {
363      const canvas = canvasRef.current;
364      if (!canvas) return { x: 0, y: 0 };
365      const rect = canvas.getBoundingClientRect();
366      const clientX = 'touches' in e ? e.touches[0].clientX : (e as React.MouseEvent).clientX;
367      const clientY = 'touches' in e ? e.touches[0].clientY : (e as React.MouseEvent).clientY;
368      return { x: clientX - rect.left, y: clientY - rect.top };
369    };
370  
371    const startDrag = (e: React.MouseEvent | React.TouchEvent) => {
372      if (!blurMode || !mediaState.loaded) return;
373      // Prevent scrolling on touch
374      // e.preventDefault(); // Done in CSS 'touch-none', but here we manage React event
375      
376      setIsDragging(true);
377      const coords = getCanvasCoords(e);
378      setDragStart(coords);
379      setDragCurrent(coords);
380    };
381  
382    const moveDrag = (e: React.MouseEvent | React.TouchEvent) => {
383      if (!isDragging || !dragStart) return;
384      setDragCurrent(getCanvasCoords(e));
385    };
386  
387    const endDrag = () => {
388      if (!isDragging || !dragStart || !dragCurrent) return;
389      const canvas = canvasRef.current;
390      if (!canvas) return;
391  
392      const rect = canvas.getBoundingClientRect();
393      const scaleX = canvas.width / rect.width;
394      const scaleY = canvas.height / rect.height;
395  
396      const cssX = Math.min(dragStart.x, dragCurrent.x);
397      const cssY = Math.min(dragStart.y, dragCurrent.y);
398      const cssW = Math.abs(dragCurrent.x - dragStart.x);
399      const cssH = Math.abs(dragCurrent.y - dragStart.y);
400  
401      if (cssW > 5 && cssH > 5) {
402        const newRegion: BlurRegion = { 
403          x: cssX * scaleX, 
404          y: cssY * scaleY, 
405          w: cssW * scaleX, 
406          h: cssH * scaleY 
407        };
408        setFilters(prev => ({ ...prev, blurRegions: [...prev.blurRegions, newRegion] }));
409      }
410  
411      setIsDragging(false);
412      setDragStart(null);
413      setDragCurrent(null);
414    };
415  
416    return (
417      <div 
418        className="min-h-screen flex flex-col font-sans antialiased selection:bg-cyan-500/30"
419        dir={isRTL ? 'rtl' : 'ltr'}
420      >
421        {/* --- Header --- */}
422        <header className="bg-slate-900/80 backdrop-blur-md border-b border-slate-800 sticky top-0 z-50">
423          <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
424            <div className="flex items-center gap-3">
425              <ShieldPlus size={32} className="text-white" />
426              <div>
427                <h1 className="font-bold text-slate-100 leading-none text-lg tracking-tight">{t.title}</h1>
428                <p className="text-[10px] text-slate-400 font-mono mt-1 uppercase tracking-wider hidden sm:block">{t.privacyNote}</p>
429              </div>
430            </div>
431  
432            <div className="flex items-center gap-2 sm:gap-4">
433              {showInstallButton && (
434                <Button
435                  variant="primary"
436                  onClick={handleInstallClick}
437                  className="gap-2 text-xs px-3 py-1.5"
438                >
439                  <Upload size={14} />
440                  <span className="hidden sm:inline">Install</span>
441                </Button>
442              )}
443  
444              <Button variant="ghost" onClick={() => setShowHelp(!showHelp)} className="p-2 rounded-full">
445                <Info size={20} />
446              </Button>
447              
448              <div className="relative">
449                 <button 
450                   onClick={() => setIsLangMenuOpen(!isLangMenuOpen)}
451                   className="flex items-center gap-1 bg-slate-800 hover:bg-slate-700 text-xs py-1.5 px-3 rounded-full border border-slate-700 transition-colors text-slate-300"
452                 >
453                   <Globe size={12} />
454                   <span className="uppercase">{lang}</span>
455                 </button>
456  
457                 {isLangMenuOpen && (
458                   <>
459                     <div className="fixed inset-0 z-10" onClick={() => setIsLangMenuOpen(false)} />
460                     <div className="absolute right-0 top-full mt-2 w-32 bg-slate-800 border border-slate-700 rounded-lg shadow-xl overflow-hidden z-20">
461                       {SUPPORTED_LANGUAGES.map(l => (
462                         <button 
463                          key={l.code}
464                          onClick={() => {
465                            setLang(l.code);
466                            setIsLangMenuOpen(false);
467                          }}
468                          className="w-full text-left px-4 py-2 text-xs hover:bg-slate-700 text-slate-300"
469                         >
470                           {l.label}
471                         </button>
472                       ))}
473                     </div>
474                   </>
475                 )}
476              </div>
477            </div>
478          </div>
479        </header>
480  
481        {/* --- Help Modal --- */}
482        {showHelp && (
483          <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm" onClick={() => setShowHelp(false)}>
484            <div className="bg-slate-900 border border-slate-700 rounded-2xl max-w-md w-full p-6 shadow-2xl" onClick={e => e.stopPropagation()}>
485              <div className="flex items-center gap-3 mb-4 text-cyan-400">
486                <Shield size={24} />
487                <h2 className="text-xl font-bold text-white">{t.helpTitle}</h2>
488              </div>
489              <p className="text-slate-300 text-sm leading-relaxed mb-6">
490                {t.helpContent}
491              </p>
492              <ul className="space-y-2 text-xs text-slate-400 mb-4 font-mono">
493                <li className="flex items-center gap-2"><CheckCircle size={12} className="text-green-500"/> {t.noUploads}</li>
494                <li className="flex items-center gap-2"><CheckCircle size={12} className="text-green-500"/> {t.exifStripped}</li>
495                <li className="flex items-center gap-2"><CheckCircle size={12} className="text-green-500"/> {t.runsOffline}</li>
496              </ul>
497              <p className="text-xs text-slate-400 mb-6">
498                {t.radicleLink}{' '}
499                <a
500                  href="https://app.radicle.xyz/nodes/rosa.radicle.xyz/rad%3Az25ksth8ffPkY4Tg5s2zeiE6xhyYC"
501                  target="_blank"
502                  rel="noopener noreferrer"
503                  className="text-cyan-400 hover:text-cyan-300 underline"
504                >
505                  rad:z25ksth8ffPkY4Tg5s2zeiE6xhyYC
506                </a>
507              </p>
508              <Button onClick={() => setShowHelp(false)} className="w-full">Got it</Button>
509            </div>
510          </div>
511        )}
512  
513        {/* --- Main Content --- */}
514        <main className="flex-1 flex flex-col lg:flex-row overflow-hidden h-[calc(100vh-64px)]">
515          
516          {/* Canvas Area */}
517          <div 
518            ref={containerRef}
519            className="flex-1 bg-slate-950 relative flex flex-col items-center justify-center p-4 lg:p-8 overflow-auto"
520            onDragOver={(e) => e.preventDefault()}
521            onDrop={(e) => {
522              e.preventDefault();
523              if (e.dataTransfer.files && e.dataTransfer.files[0]) {
524                const dt = new DataTransfer();
525                dt.items.add(e.dataTransfer.files[0]);
526                if (fileInputRef.current) {
527                  fileInputRef.current.files = dt.files;
528                  const event = new Event('change', { bubbles: true });
529                  fileInputRef.current.dispatchEvent(event);
530                }
531              }
532            }}
533          >
534            {!mediaState.loaded ? (
535              <div 
536                onClick={() => fileInputRef.current?.click()}
537                className="border-2 border-dashed border-slate-700 rounded-3xl p-12 text-center max-w-md w-full hover:border-cyan-500/50 hover:bg-slate-900/50 transition-all cursor-pointer group"
538              >
539                <div className="w-16 h-16 bg-slate-800 rounded-full flex items-center justify-center mx-auto mb-4 group-hover:scale-110 transition-transform">
540                  <Upload className="text-slate-400 group-hover:text-cyan-400" size={24} />
541                </div>
542                <h3 className="text-lg font-medium text-slate-200 mb-2">{t.openButton}</h3>
543                <p className="text-sm text-slate-500">{t.placeholder}</p>
544              </div>
545            ) : (
546              <div className="flex flex-col gap-4 w-full max-w-4xl items-center">
547                <div className="relative shadow-2xl shadow-black/50 rounded-sm overflow-hidden inline-flex flex-col bg-black">
548                  {/* Canvas */}
549                  <canvas
550                    ref={canvasRef}
551                    className={`max-w-full max-h-[50vh] lg:max-h-[70vh] object-contain block touch-none ${blurMode ? 'cursor-crosshair' : 'cursor-default'}`}
552                    onMouseDown={startDrag}
553                    onMouseMove={moveDrag}
554                    onMouseUp={endDrag}
555                    onMouseLeave={endDrag}
556                    onTouchStart={startDrag}
557                    onTouchMove={moveDrag}
558                    onTouchEnd={endDrag}
559                  />
560                  
561                  {/* Recording Indicator */}
562                  {mediaState.isRecording && (
563                    <div className="absolute top-4 right-4 flex items-center gap-2 bg-red-500/90 text-white text-xs font-bold px-3 py-1.5 rounded-full animate-pulse shadow-lg z-20">
564                      <div className="w-2 h-2 bg-white rounded-full"></div>
565                      REC
566                    </div>
567                  )}
568  
569                  {/* Drag Box */}
570                  {isDragging && dragStart && dragCurrent && (
571                    <div 
572                      className="absolute border-2 border-yellow-400 bg-yellow-400/20 pointer-events-none z-10 shadow-[0_0_10px_rgba(250,204,21,0.3)]"
573                      style={{
574                        left: Math.min(dragStart.x, dragCurrent.x),
575                        top: Math.min(dragStart.y, dragCurrent.y),
576                        width: Math.abs(dragCurrent.x - dragStart.x),
577                        height: Math.abs(dragCurrent.y - dragStart.y),
578                      }} 
579                    />
580                  )}
581                </div>
582  
583                {/* Persistent Video Controls */}
584                {mediaState.type === 'video' && (
585                  <div className="flex items-center gap-4 bg-slate-900 border border-slate-800 rounded-2xl p-2 shadow-xl w-full max-w-md">
586                    <Button 
587                      variant="ghost" 
588                      onClick={handleVideoToggle} 
589                      className="rounded-xl w-12 h-12 flex-shrink-0 hover:bg-slate-800"
590                      title={t.playPause}
591                    >
592                      {mediaState.isPlaying ? <Pause size={24} className="fill-current" /> : <Play size={24} className="fill-current" />}
593                    </Button>
594                    
595                    <div className="flex-1 h-1 bg-slate-800 rounded-full overflow-hidden relative">
596                      {/* Simple static progress bar visual for now, since we are looping/processing */}
597                      <div className={`absolute top-0 bottom-0 left-0 bg-cyan-500 ${mediaState.isPlaying ? 'w-full animate-[progress_2s_linear_infinite]' : 'w-1/2'}`}></div>
598                    </div>
599  
600                    <Button
601                      variant="ghost"
602                      onClick={() => setAudioEnabled(!audioEnabled)}
603                      className={`rounded-xl w-12 h-12 flex-shrink-0 ${audioEnabled ? 'text-yellow-500 hover:bg-yellow-500/10' : 'text-red-500 hover:bg-red-500/10'}`}
604                      title={audioEnabled ? "Audio ON" : "Audio OFF"}
605                      disabled={mediaState.isRecording}
606                    >
607                      <VolumeX size={24} />
608                    </Button>
609  
610                    <Button
611                      variant={mediaState.isRecording ? "danger" : "primary"}
612                      onClick={handleSave}
613                      className="rounded-xl px-6 py-2 text-xs font-bold uppercase tracking-wider min-w-[120px]"
614                    >
615                      {mediaState.isRecording ? t.stopRec : t.record}
616                    </Button>
617                  </div>
618                )}
619              </div>
620            )}
621            
622            <input
623              type="file"
624              ref={fileInputRef}
625              onChange={handleFileSelect}
626              accept="image/*,video/*"
627              className="hidden"
628            />
629          </div>
630  
631          {/* Controls Sidebar */}
632          <div className="w-full lg:w-80 bg-slate-900 border-t lg:border-t-0 lg:border-l border-slate-800 p-6 flex flex-col gap-6 shadow-2xl z-40 overflow-y-auto h-1/2 lg:h-auto shrink-0">
633            
634            {/* Status */}
635            <div className="flex items-center justify-between bg-slate-950/50 p-3 rounded-xl border border-slate-800 shrink-0">
636               <span className="text-xs font-mono text-slate-400 uppercase tracking-wider">{t.statusLabel}</span>
637               <div className="flex items-center gap-2">
638                 <span className={`w-2 h-2 rounded-full ${mediaState.isRecording ? 'bg-red-500 animate-pulse' : mediaState.loaded ? 'bg-green-500' : 'bg-slate-600'}`}></span>
639                 <span className="text-xs font-medium text-slate-200">
640                    {mediaState.isRecording ? t.statusRecording : mediaState.loaded ? t.statusReady : t.statusNoImage}
641                 </span>
642               </div>
643            </div>
644  
645            {/* Privacy Guarantee Message */}
646            {!mediaState.loaded && (
647              <div className="flex flex-col gap-3 p-5 bg-gradient-to-br from-slate-800/50 to-slate-900/50 rounded-2xl border border-slate-700/50">
648                <div className="flex items-center gap-2">
649                  <Shield size={18} className="text-cyan-500" />
650                  <h3 className="text-sm font-semibold text-slate-200">{t.privacyTitle}</h3>
651                </div>
652                <p className="text-xs leading-relaxed text-slate-400">
653                  {t.privacyMessage}
654                </p>
655              </div>
656            )}
657  
658            {/* Conditional Content based on Load State */}
659            {mediaState.loaded && (
660              <div className="space-y-6 overflow-y-auto pr-2 custom-scrollbar flex-1">
661                
662                {/* Anonymize Tools (Priority) */}
663                <div>
664                   <h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-3 flex items-center gap-2">
665                    {t.anonymizeSection}
666                  </h3>
667                  <Button 
668                    variant={blurMode ? "primary" : "secondary"} 
669                    className="w-full justify-start gap-3"
670                    onClick={() => setBlurMode(!blurMode)}
671                    disabled={mediaState.isRecording}
672                  >
673                    <div className={`w-2 h-2 rounded-full ${blurMode ? 'bg-white animate-pulse' : 'bg-transparent'}`} />
674                    {t.blurTool}
675                  </Button>
676                  
677                   {filters.blurRegions.length > 0 && (
678                    <div className="text-xs text-slate-500 text-center mt-2">
679                      {filters.blurRegions.length} {t.areasBlurred}
680                    </div>
681                  )}
682                </div>
683  
684                {/* Global Filters */}
685                <div>
686                  <h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-3 flex items-center gap-2">
687                    {t.globalFiltersSection}
688                  </h3>
689                  <div className="space-y-5 p-4 bg-slate-800/30 rounded-2xl border border-slate-800/50">
690                    <Slider 
691                      label={t.grayLabel} 
692                      value={filters.grayscale} 
693                      onChange={(v) => setFilters(prev => ({ ...prev, grayscale: v }))}
694                      disabled={mediaState.isRecording}
695                    />
696                    <Slider 
697                      label={t.noiseLabel} 
698                      value={filters.noise} 
699                      onChange={(v) => setFilters(prev => ({ ...prev, noise: v }))}
700                      disabled={mediaState.isRecording}
701                    />
702                  </div>
703                </div>
704  
705                {/* Glitch / VFX */}
706                <div>
707                  <h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-3 flex items-center gap-2">
708                    {t.glitchSection}
709                  </h3>
710                  <div className="space-y-5 p-4 bg-slate-800/30 rounded-2xl border border-slate-800/50">
711                    <Slider 
712                      label={t.scanlineLabel} 
713                      value={filters.scanlines} 
714                      onChange={(v) => setFilters(prev => ({ ...prev, scanlines: v }))}
715                      disabled={mediaState.isRecording}
716                    />
717                    <Slider 
718                      label={t.rgbShiftLabel} 
719                      value={filters.rgbShift} 
720                      onChange={(v) => setFilters(prev => ({ ...prev, rgbShift: v }))}
721                      disabled={mediaState.isRecording}
722                    />
723                    <Slider 
724                      label={t.glitchIntensityLabel} 
725                      value={filters.glitchIntensity} 
726                      onChange={(v) => setFilters(prev => ({ ...prev, glitchIntensity: v }))}
727                      disabled={mediaState.isRecording}
728                    />
729                  </div>
730                </div>
731              </div>
732            )}
733  
734            {/* Actions Footer */}
735            <div className="mt-auto space-y-3 pt-4 border-t border-slate-800 shrink-0">
736              {/* Main Save Button - Logic varies by Type */}
737              {mediaState.type === 'image' ? (
738                <Button 
739                  variant="primary" 
740                  className="w-full gap-2 py-4 text-base"
741                  onClick={handleSave}
742                  disabled={!mediaState.loaded}
743                >
744                  <Save size={18} />
745                  {t.save}
746                </Button>
747              ) : (
748                <Button
749                   variant="secondary"
750                   className="w-full gap-2 py-4 text-base opacity-50 cursor-not-allowed"
751                   disabled={true}
752                >
753                  <Info size={14} />
754                  {t.useControlsAbove}
755                </Button>
756              )}
757  
758              <div className="grid grid-cols-2 gap-3">
759                 <Button 
760                  variant="danger" 
761                  className="w-full gap-2"
762                  onClick={handleReset}
763                  disabled={!mediaState.loaded || mediaState.isRecording}
764                >
765                  <RefreshCw size={14} />
766                  {t.reset}
767                </Button>
768                <Button 
769                  variant="secondary"
770                  className="w-full gap-2"
771                  onClick={() => fileInputRef.current?.click()}
772                  disabled={mediaState.isRecording}
773                >
774                  <Upload size={14} />
775                  {t.openButton}
776                </Button>
777              </div>
778            </div>
779  
780          </div>
781        </main>
782      </div>
783    );
784  }
785  
786  export default App;