// DOM Elements const screenshotImg = document.getElementById('screenshotImg'); const browserViewport = document.getElementById('browserViewport'); const loadingOverlay = document.getElementById('loadingOverlay'); const loadingText = document.getElementById('loadingText'); const urlInput = document.getElementById('urlInput'); const goBtn = document.getElementById('goBtn'); const backBtn = document.getElementById('backBtn'); const forwardBtn = document.getElementById('forwardBtn'); const reloadBtn = document.getElementById('reloadBtn'); const keyboardCapture = document.getElementById('keyboardCapture'); const statusDot = document.querySelector('.status-dot'); const statusText = document.getElementById('statusText'); const currentUrlEl = document.getElementById('currentUrl'); const pageTitleEl = document.getElementById('pageTitle'); const latencyEl = document.getElementById('latency'); // Toast const toast = document.getElementById('toast'); const toastIcon = document.getElementById('toastIcon'); const toastMessage = document.getElementById('toastMessage'); // State let isLoading = false; let lastRequestTime = 0; let isBrowserReady = false; let autoRefreshInterval = null; // Toast function function showToast(message, type = 'success') { toastMessage.textContent = message; toastIcon.className = 'toast-icon fas '; if (type === 'success') { toastIcon.classList.add('fa-check-circle', 'success'); } else if (type === 'error') { toastIcon.classList.add('fa-exclamation-circle', 'error'); } else { toastIcon.classList.add('fa-info-circle', 'info'); } toast.classList.add('show'); setTimeout(() => { toast.classList.remove('show'); }, 3000); } // Update status function updateStatus(status, text) { statusDot.className = 'status-dot'; if (status === 'connected') { statusDot.classList.add('connected'); } else if (status === 'error') { statusDot.classList.add('error'); } statusText.textContent = text; } // Show loading function showLoading(text = '加载中...') { loadingText.textContent = text; loadingOverlay.classList.remove('hidden'); isLoading = true; } // Hide loading function hideLoading() { loadingOverlay.classList.add('hidden'); isLoading = false; } // Update screenshot function updateScreenshot(data) { if (data.screenshot) { screenshotImg.src = 'data:image/jpeg;base64,' + data.screenshot; } if (data.url) { currentUrlEl.textContent = data.url; urlInput.value = data.url; } if (data.title) { pageTitleEl.textContent = data.title; } // Calculate latency if (lastRequestTime > 0) { const latency = Date.now() - lastRequestTime; latencyEl.textContent = latency + 'ms'; } } // Navigate to URL async function navigateTo(url) { if (isLoading) return; showLoading('正在加载页面...'); lastRequestTime = Date.now(); try { const response = await fetch('/api/navigate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: url }) }); const data = await response.json(); if (data.success) { updateScreenshot(data); hideLoading(); showToast('页面加载成功', 'success'); } else { hideLoading(); showToast(data.error || '加载失败', 'error'); } } catch (error) { hideLoading(); showToast('网络错误: ' + error.message, 'error'); console.error('Navigate error:', error); } } // Get screenshot async function refreshScreenshot() { if (isLoading) return; lastRequestTime = Date.now(); try { const response = await fetch('/api/screenshot'); const data = await response.json(); if (data.success) { updateScreenshot(data); return true; } return false; } catch (error) { console.error('Screenshot error:', error); return false; } } // Handle click async function handleClick(x, y) { if (isLoading || !isBrowserReady) return; lastRequestTime = Date.now(); try { const response = await fetch('/api/click', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ x: x, y: y }) }); const data = await response.json(); if (data.success) { updateScreenshot(data); } } catch (error) { console.error('Click error:', error); } } // Handle type async function handleType(text) { if (isLoading || !isBrowserReady) return; lastRequestTime = Date.now(); try { const response = await fetch('/api/type', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: text }) }); const data = await response.json(); if (data.success) { updateScreenshot(data); } } catch (error) { console.error('Type error:', error); } } // Handle keypress async function handleKeypress(key) { if (isLoading || !isBrowserReady) return; lastRequestTime = Date.now(); try { const response = await fetch('/api/keypress', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key: key }) }); const data = await response.json(); if (data.success) { updateScreenshot(data); } } catch (error) { console.error('Keypress error:', error); } } // Handle scroll async function handleScroll(deltaX, deltaY) { if (isLoading || !isBrowserReady) return; lastRequestTime = Date.now(); try { const response = await fetch('/api/scroll', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ deltaX: deltaX, deltaY: deltaY }) }); const data = await response.json(); if (data.success) { updateScreenshot(data); } } catch (error) { console.error('Scroll error:', error); } } // Navigation functions async function goBack() { if (isLoading) return; showLoading('后退中...'); lastRequestTime = Date.now(); try { const response = await fetch('/api/go-back', { method: 'POST' }); const data = await response.json(); if (data.success) { updateScreenshot(data); hideLoading(); } else { hideLoading(); showToast(data.error || '操作失败', 'error'); } } catch (error) { hideLoading(); showToast('网络错误', 'error'); } } async function goForward() { if (isLoading) return; showLoading('前进中...'); lastRequestTime = Date.now(); try { const response = await fetch('/api/go-forward', { method: 'POST' }); const data = await response.json(); if (data.success) { updateScreenshot(data); hideLoading(); } else { hideLoading(); showToast(data.error || '操作失败', 'error'); } } catch (error) { hideLoading(); showToast('网络错误', 'error'); } } async function reload() { if (isLoading) return; showLoading('刷新中...'); lastRequestTime = Date.now(); try { const response = await fetch('/api/reload', { method: 'POST' }); const data = await response.json(); if (data.success) { updateScreenshot(data); hideLoading(); } else { hideLoading(); showToast(data.error || '刷新失败', 'error'); } } catch (error) { hideLoading(); showToast('网络错误', 'error'); } } // Calculate click coordinates function getClickCoordinates(event) { const rect = screenshotImg.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; // Calculate scale const scaleX = viewportWidth / rect.width; const scaleY = viewportHeight / rect.height; // Use the smaller scale to maintain aspect ratio const scale = Math.min(scaleX, scaleY); // Calculate actual coordinates in the viewport const actualX = Math.round(x * scale); const actualY = Math.round(y * scale); return { x: actualX, y: actualY }; } // Event Listeners // URL input goBtn.addEventListener('click', () => { const url = urlInput.value.trim(); if (url) { navigateTo(url); } }); urlInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { const url = urlInput.value.trim(); if (url) { navigateTo(url); } } }); // Navigation buttons backBtn.addEventListener('click', goBack); forwardBtn.addEventListener('click', goForward); reloadBtn.addEventListener('click', reload); // Viewport click browserViewport.addEventListener('click', (e) => { if (!isBrowserReady) return; // Focus the hidden input to capture keyboard keyboardCapture.focus(); const coords = getClickCoordinates(e); handleClick(coords.x, coords.y); }); // Keyboard input keyboardCapture.addEventListener('input', (e) => { const text = e.target.value; if (text) { handleType(text); e.target.value = ''; } }); keyboardCapture.addEventListener('keydown', (e) => { // Handle special keys const specialKeys = ['Enter', 'Backspace', 'Tab', 'Escape', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']; if (specialKeys.includes(e.key)) { e.preventDefault(); handleKeypress(e.key); } }); // Scroll browserViewport.addEventListener('wheel', (e) => { e.preventDefault(); handleScroll(e.deltaX, e.deltaY); }, { passive: false }); // Initialize async function init() { updateStatus('connecting', '连接中...'); showLoading('正在启动云浏览器...'); // Check browser status try { const response = await fetch('/api/status'); const data = await response.json(); if (data.ready) { isBrowserReady = true; updateStatus('connected', '已连接'); currentUrlEl.textContent = data.url || '-'; pageTitleEl.textContent = data.title || '-'; // Get initial screenshot await refreshScreenshot(); hideLoading(); } else { // Try to initialize by getting screenshot showLoading('正在初始化浏览器...'); const success = await refreshScreenshot(); if (success) { isBrowserReady = true; updateStatus('connected', '已连接'); hideLoading(); } else { updateStatus('error', '连接失败'); loadingText.textContent = '浏览器启动失败,请刷新页面重试'; } } } catch (error) { updateStatus('error', '连接失败'); loadingText.textContent = '无法连接到服务器'; console.error('Init error:', error); } } // Start document.addEventListener('DOMContentLoaded', init);