import { X, ArrowLeft, ArrowRight, RotateCw, Plus, Globe, Maximize2, Minimize2, ShieldCheck, Lock, Camera, Scissors } from 'lucide-react'; import { useAppStore } from '../store'; import { useState, useRef, useEffect, useCallback } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; const bookmarks = [ { name: 'ArtStation', icon: 'ðŸŽĻ', url: 'https://artstation.com' }, { name: 'Pinterest', icon: '📌', url: 'https://pinterest.com' }, { name: 'Unsplash', icon: '📷', url: 'https://unsplash.com' }, { name: 'Sketchfab', icon: 'ðŸ“Ķ', url: 'https://sketchfab.com' }, { name: 'Google Images', icon: '🔍', url: 'https://images.google.com' }, { name: 'PolyHaven', icon: '🌍', url: 'https://polyhaven.com' }, { name: 'Anatomy360', icon: 'ðŸĶī', url: 'https://anatomy360.info' }, { name: 'Line of Action', icon: '🏃', url: 'https://line-of-action.com' }, ]; let browserInitPromise: Promise | null = null; function ensureBrowserInit() { if (!browserInitPromise) { browserInitPromise = invoke('browser_init', { layout: { x: 0, y: 0, width: 1, height: 1 } }).catch((err) => { console.error('[BrowserPanel] browser_init failed', err); browserInitPromise = null; return null; }); } return browserInitPromise; } export const BrowserPanel = () => { const { isBrowserOpen, setIsBrowserOpen, isWhisperBrowser, setImages, pan, zoom } = useAppStore(); const [url, setUrl] = useState('https://unsplash.com'); const [isFullscreen, setIsFullscreen] = useState(false); const [canGoBack, setCanGoBack] = useState(false); const [canGoForward, setCanGoForward] = useState(false); const [activeTabId, setActiveTabId] = useState(null); const [webviewVisible, setWebviewVisible] = useState(false); const [isBrowserReady, setIsBrowserReady] = useState(false); const [isCapturing, setIsCapturing] = useState(false); const [isSnipping, setIsSnipping] = useState(false); const [frozenFrame, setFrozenFrame] = useState(null); const [snipStart, setSnipStart] = useState<{x:number;y:number} | null>(null); const [snipCurrent, setSnipCurrent] = useState<{x:number;y:number} | null>(null); const contentRef = useRef(null); const syncTimerRef = useRef(null); const computeLayout = useCallback(() => { const el = contentRef.current; if (!el) return null; const rect = el.getBoundingClientRect(); if (rect.width < 80 || rect.height < 80) return null; return { x: rect.left, y: rect.top, width: rect.width, height: rect.height }; }, []); const hideWebview = useCallback(() => { if (syncTimerRef.current) window.clearTimeout(syncTimerRef.current); // Hard-hide all tabs, not just active tab. Native child webviews are independent // of the React shell and can otherwise remain visible after drawer close. invoke('browser_hide_all').catch(() => { invoke('browser_set_visible', { visible: false, layout: { x: 0, y: 0, width: 1, height: 1 } }).catch(() => {}); }); setWebviewVisible(false); }, []); const syncWebviewNow = useCallback(async () => { if (!isBrowserOpen || isSnipping) return; const layout = computeLayout(); if (!layout) return; const snap = await ensureBrowserInit(); if (snap?.active && !activeTabId) setActiveTabId(snap.active); try { await invoke('browser_set_visible', { visible: true, layout }); setWebviewVisible(true); } catch (err) { console.error('[BrowserPanel] browser_set_visible failed', err); } }, [isBrowserOpen, isSnipping, computeLayout, activeTabId]); const scheduleSync = useCallback((delay = 0) => { if (syncTimerRef.current) window.clearTimeout(syncTimerRef.current); syncTimerRef.current = window.setTimeout(() => requestAnimationFrame(() => syncWebviewNow()), delay); }, [syncWebviewNow]); useEffect(() => { ensureBrowserInit().then((snap) => { if (snap?.active) setActiveTabId(snap.active); setIsBrowserReady(true); }); return () => hideWebview(); }, [hideWebview]); useEffect(() => { if (!isBrowserOpen) { hideWebview(); return; } scheduleSync(50); const timers = [160, 300, 450, 700, 1100].map(ms => window.setTimeout(() => scheduleSync(0), ms)); return () => timers.forEach(t => window.clearTimeout(t)); }, [isBrowserOpen, scheduleSync, hideWebview]); useEffect(() => { if (!isBrowserOpen) return; hideWebview(); scheduleSync(80); const timers = [220, 420, 800].map(ms => window.setTimeout(() => scheduleSync(0), ms)); return () => timers.forEach(t => window.clearTimeout(t)); }, [isFullscreen, isWhisperBrowser, isBrowserOpen, hideWebview, scheduleSync]); useEffect(() => { const ro = new ResizeObserver(() => { if (isBrowserOpen && !isSnipping) scheduleSync(20); }); if (contentRef.current) ro.observe(contentRef.current); const onResize = () => { if (isBrowserOpen && !isSnipping) scheduleSync(20); }; window.addEventListener('resize', onResize); return () => { ro.disconnect(); window.removeEventListener('resize', onResize); }; }, [isBrowserOpen, isSnipping, scheduleSync]); useEffect(() => { const unlisten = listen('browser://tabs', (event) => { const snap = event.payload; const active = snap.tabs?.find((t: any) => t.id === snap.active); if (active) { setUrl(active.url || ''); setCanGoBack(Boolean(active.can_go_back)); setCanGoForward(Boolean(active.can_go_forward)); setActiveTabId(snap.active || null); } }); return () => { unlisten.then(fn => fn()); }; }, []); const navigate = async (targetUrl: string) => { const snap = await ensureBrowserInit(); const tabId = activeTabId || snap?.active; if (tabId) invoke('tab_navigate', { tabId, url: targetUrl }).catch(console.error); }; const goBack = () => { if (activeTabId) invoke('tab_back', { tabId: activeTabId }).catch(() => {}); }; const goForward = () => { if (activeTabId) invoke('tab_forward', { tabId: activeTabId }).catch(() => {}); }; const reload = () => { if (activeTabId) invoke('tab_reload', { tabId: activeTabId }).catch(() => {}); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); navigate(url); }; function addCapturedImage(dataUrl: string, w: number, h: number) { const ratio = Math.max(0.1, w / h); const tw = Math.min(w, 800); setImages(prev => [...prev, { id: crypto.randomUUID(), url: dataUrl, sourceUrl: url, x: (-pan.x + window.innerWidth / 2 - tw / 2) / zoom, y: (-pan.y + window.innerHeight / 2 - (tw / ratio) / 2) / zoom, width: Math.round(tw), height: Math.round(tw / ratio), aspectRatio: ratio }]); } const captureViewport = async () => { const layout = computeLayout(); if (!layout || isCapturing) return; setIsCapturing(true); try { const dataUrl = await invoke('browser_capture_viewport', { layout }); addCapturedImage(dataUrl, layout.width, layout.height); } catch (err) { console.error('[BrowserPanel] viewport capture failed', err); } finally { setIsCapturing(false); } }; const startSnip = async () => { const layout = computeLayout(); if (!layout || isCapturing || isSnipping) return; setIsCapturing(true); try { const frame = await invoke('browser_capture_viewport', { layout }); hideWebview(); setFrozenFrame(frame); setIsSnipping(true); } catch (err) { console.error('[BrowserPanel] snip prep failed', err); } finally { setIsCapturing(false); } }; const cancelSnip = () => { setIsSnipping(false); setFrozenFrame(null); setSnipStart(null); setSnipCurrent(null); scheduleSync(0); }; const onSnipDown = (e: React.PointerEvent) => { const r = e.currentTarget.getBoundingClientRect(); setSnipStart({x:e.clientX-r.left,y:e.clientY-r.top}); setSnipCurrent({x:e.clientX-r.left,y:e.clientY-r.top}); }; const onSnipMove = (e: React.PointerEvent) => { if (!snipStart) return; const r = e.currentTarget.getBoundingClientRect(); setSnipCurrent({x:e.clientX-r.left,y:e.clientY-r.top}); }; const onSnipUp = (e: React.PointerEvent) => { if (!snipStart || !snipCurrent || !frozenFrame) { cancelSnip(); return; } const r = e.currentTarget.getBoundingClientRect(); const ex=e.clientX-r.left, ey=e.clientY-r.top; const sx=Math.min(snipStart.x,ex), sy=Math.min(snipStart.y,ey); const sw=Math.abs(ex-snipStart.x), sh=Math.abs(ey-snipStart.y); if (sw<20||sh<20){cancelSnip();return;} const img=new Image(); img.onload=()=>{const scaleX=img.width/r.width, scaleY=img.height/r.height; const canvas=document.createElement('canvas'); canvas.width=Math.round(sw*scaleX); canvas.height=Math.round(sh*scaleY); const ctx=canvas.getContext('2d'); if(ctx){ctx.drawImage(img,Math.round(sx*scaleX),Math.round(sy*scaleY),canvas.width,canvas.height,0,0,canvas.width,canvas.height); addCapturedImage(canvas.toDataURL('image/png'),sw,sh);} cancelSnip();}; img.onerror=cancelSnip; img.src=frozenFrame; }; useEffect(() => { if (!isSnipping) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') cancelSnip(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [isSnipping]); const widthClass = isFullscreen ? 'w-[100vw]' : isWhisperBrowser ? 'w-[320px] min-w-[280px]' : 'w-[50vw] min-w-[400px] max-w-[700px]'; return (
setUrl(e.target.value)} className="w-full bg-transparent text-[#E0E0E0] pl-14 pr-8 py-2 text-[13px] outline-none" spellCheck={false} />{url && }
{!isWhisperBrowser &&
{bookmarks.map((bm, i) => )}
}
{!webviewVisible &&

{isBrowserReady ? 'Positioning browser...' : 'Starting browser...'}

} {isSnipping && frozenFrame &&
{snipStart && snipCurrent &&
}
Drag to select area â€Ē Esc to cancel
}
); };