Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Screen Monitor Pro</title> | |
| <script src="https://unpkg.com/tesseract.js@v2.1.0/dist/tesseract.min.js"></script> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: -apple-system, system-ui, sans-serif; | |
| } | |
| .app-container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| background: #f8fafc; | |
| min-height: 100vh; | |
| } | |
| .header { | |
| background: white; | |
| padding: 20px; | |
| border-radius: 8px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| text-align: center; | |
| } | |
| .status-bar { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background: white; | |
| padding: 15px; | |
| border-radius: 8px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .status-indicator { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .status-dot { | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| background: #cbd5e1; | |
| } | |
| .status-dot.active { | |
| background: #22c55e; | |
| animation: pulse 1s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| 100% { opacity: 1; } | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| button { | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-weight: 500; | |
| transition: all 0.2s; | |
| } | |
| button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .start-btn { | |
| background: #22c55e; | |
| color: white; | |
| } | |
| .stop-btn { | |
| background: #ef4444; | |
| color: white; | |
| } | |
| .preview-container { | |
| background: black; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| margin-bottom: 20px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| position: relative; | |
| } | |
| #preview { | |
| width: 100%; | |
| aspect-ratio: 16/9; | |
| object-fit: contain; | |
| } | |
| .logs { | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| overflow: hidden; | |
| } | |
| .log-header { | |
| background: #1e293b; | |
| color: white; | |
| padding: 15px; | |
| font-weight: 500; | |
| } | |
| .log-entry { | |
| padding: 20px; | |
| border-bottom: 1px solid #e2e8f0; | |
| display: grid; | |
| grid-template-columns: 300px 1fr; | |
| gap: 20px; | |
| } | |
| .screenshot-container { | |
| position: relative; | |
| } | |
| .screenshot { | |
| width: 100%; | |
| border-radius: 4px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .change-highlight { | |
| position: absolute; | |
| border: 2px solid #ef4444; | |
| background: rgba(239, 68, 68, 0.2); | |
| pointer-events: none; | |
| transition: all 0.3s; | |
| } | |
| .info-panel { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| } | |
| .timestamp { | |
| color: #64748b; | |
| font-size: 0.9rem; | |
| } | |
| .analysis { | |
| background: #f8fafc; | |
| padding: 15px; | |
| border-radius: 6px; | |
| font-size: 0.95rem; | |
| line-height: 1.5; | |
| } | |
| .analysis h4 { | |
| margin-bottom: 8px; | |
| color: #1e293b; | |
| } | |
| .ocr-text { | |
| font-family: monospace; | |
| white-space: pre-wrap; | |
| background: #f1f5f9; | |
| padding: 10px; | |
| border-radius: 4px; | |
| margin-top: 8px; | |
| } | |
| .actions { | |
| display: flex; | |
| gap: 10px; | |
| margin-top: auto; | |
| } | |
| .download-btn { | |
| padding: 8px 16px; | |
| background: #3b82f6; | |
| color: white; | |
| text-decoration: none; | |
| border-radius: 4px; | |
| font-size: 0.9rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .error-message { | |
| background: #fef2f2; | |
| color: #ef4444; | |
| padding: 10px; | |
| border-radius: 4px; | |
| margin: 10px 0; | |
| display: none; | |
| } | |
| @media (max-width: 768px) { | |
| .log-entry { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <div class="header"> | |
| <h1>Screen Monitor Pro</h1> | |
| <p>Advanced screen capture and analysis</p> | |
| </div> | |
| <div class="status-bar"> | |
| <div class="status-indicator"> | |
| <div class="status-dot" id="statusDot"></div> | |
| <span id="statusText">Ready</span> | |
| </div> | |
| <div class="controls"> | |
| <button class="start-btn" id="startBtn">Start Capture</button> | |
| <button class="stop-btn" id="stopBtn" disabled>Stop</button> | |
| </div> | |
| </div> | |
| <div class="error-message" id="errorMessage"></div> | |
| <div class="preview-container"> | |
| <video id="preview" autoplay></video> | |
| </div> | |
| <div class="logs"> | |
| <div class="log-header">Change Log</div> | |
| <div id="logContainer"></div> | |
| </div> | |
| </div> | |
| <script> | |
| class ScreenAnalyzer { | |
| constructor() { | |
| this.mediaStream = null; | |
| this.captureInterval = null; | |
| this.lastImageData = null; | |
| this.worker = null; | |
| this.isProcessing = false; | |
| this.initializeOCR(); | |
| this.bindElements(); | |
| this.bindEvents(); | |
| } | |
| async initializeOCR() { | |
| try { | |
| this.worker = await Tesseract.createWorker(); | |
| await this.worker.loadLanguage('eng'); | |
| await this.worker.initialize('eng'); | |
| } catch (error) { | |
| this.showError('Failed to initialize OCR'); | |
| } | |
| } | |
| bindElements() { | |
| this.elements = { | |
| startBtn: document.getElementById('startBtn'), | |
| stopBtn: document.getElementById('stopBtn'), | |
| preview: document.getElementById('preview'), | |
| logContainer: document.getElementById('logContainer'), | |
| statusDot: document.getElementById('statusDot'), | |
| statusText: document.getElementById('statusText'), | |
| errorMessage: document.getElementById('errorMessage') | |
| }; | |
| } | |
| bindEvents() { | |
| this.elements.startBtn.addEventListener('click', () => this.start()); | |
| this.elements.stopBtn.addEventListener('click', () => this.stop()); | |
| } | |
| updateStatus(status, isError = false) { | |
| this.elements.statusDot.className = `status-dot ${status === 'recording' ? 'active' : ''}`; | |
| this.elements.statusText.textContent = status.charAt(0).toUpperCase() + status.slice(1); | |
| if (isError) { | |
| this.elements.errorMessage.textContent = status; | |
| this.elements.errorMessage.style.display = 'block'; | |
| } else { | |
| this.elements.errorMessage.style.display = 'none'; | |
| } | |
| } | |
| async start() { | |
| try { | |
| this.mediaStream = await navigator.mediaDevices.getDisplayMedia({ | |
| video: { cursor: "always" } | |
| }); | |
| this.elements.preview.srcObject = this.mediaStream; | |
| this.elements.startBtn.disabled = true; | |
| this.elements.stopBtn.disabled = false; | |
| this.updateStatus('recording'); | |
| this.captureInterval = setInterval(() => this.capture(), 1000); | |
| this.mediaStream.getVideoTracks()[0].onended = () => this.stop(); | |
| } catch (error) { | |
| this.showError('Failed to start screen capture'); | |
| } | |
| } | |
| stop() { | |
| if (this.mediaStream) { | |
| this.mediaStream.getTracks().forEach(track => track.stop()); | |
| this.elements.preview.srcObject = null; | |
| } | |
| clearInterval(this.captureInterval); | |
| this.elements.startBtn.disabled = false; | |
| this.elements.stopBtn.disabled = true; | |
| this.lastImageData = null; | |
| this.updateStatus('ready'); | |
| } | |
| async capture() { | |
| if (this.isProcessing) return; | |
| this.isProcessing = true; | |
| try { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = this.elements.preview.videoWidth; | |
| canvas.height = this.elements.preview.videoHeight; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(this.elements.preview, 0, 0); | |
| const currentImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| const changes = this.detectChanges(currentImageData); | |
| if (changes.length > 0 || !this.lastImageData) { | |
| const imageUrl = canvas.toDataURL('image/jpeg', 0.8); | |
| const ocrResults = await this.analyzeChanges(canvas, changes); | |
| this.addLogEntry(imageUrl, changes, ocrResults); | |
| } | |
| this.lastImageData = currentImageData; | |
| } catch (error) { | |
| this.showError('Failed to process capture'); | |
| } finally { | |
| this.isProcessing = false; | |
| } | |
| } | |
| detectChanges(currentImageData) { | |
| if (!this.lastImageData) return []; | |
| const changes = []; | |
| const blockSize = 20; | |
| const threshold = 30; | |
| const width = currentImageData.width; | |
| const height = currentImageData.height; | |
| for (let y = 0; y < height; y += blockSize) { | |
| for (let x = 0; x < width; x += blockSize) { | |
| let diffCount = 0; | |
| const maxY = Math.min(y + blockSize, height); | |
| const maxX = Math.min(x + blockSize, width); | |
| for (let py = y; py < maxY; py++) { | |
| for (let px = x; px < maxX; px++) { | |
| const i = (py * width + px) * 4; | |
| if (Math.abs(currentImageData.data[i] - this.lastImageData.data[i]) > threshold || | |
| Math.abs(currentImageData.data[i + 1] - this.lastImageData.data[i + 1]) > threshold || | |
| Math.abs(currentImageData.data[i + 2] - this.lastImageData.data[i + 2]) > threshold) { | |
| diffCount++; | |
| } | |
| } | |
| } | |
| if (diffCount > (blockSize * blockSize * 0.3)) { | |
| changes.push({ | |
| x: x / width * 100, | |
| y: y / height * 100, | |
| width: Math.min(blockSize / width * 100, 100 - x / width * 100), | |
| height: Math.min(blockSize / height * 100, 100 - y / height * 100), | |
| pixels: {x, y, width: maxX - x, height: maxY - y} | |
| }); | |
| } | |
| } | |
| } | |
| return changes; | |
| } | |
| async analyzeChanges(canvas, changes) { | |
| const results = []; | |
| for (const change of changes) { | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = change.pixels.width; | |
| tempCanvas.height = change.pixels.height; | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| tempCtx.drawImage( | |
| canvas, | |
| change.pixels.x, change.pixels.y, | |
| change.pixels.width, change.pixels.height, | |
| 0, 0, | |
| change.pixels.width, change.pixels.height | |
| ); | |
| try { | |
| const result = await this.worker.recognize(tempCanvas); | |
| if (result.data.text.trim()) { | |
| results.push({ | |
| text: result.data.text.trim(), | |
| confidence: result.data.confidence, | |
| region: change | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('OCR Error:', error); | |
| } | |
| } | |
| return results; | |
| } | |
| addLogEntry(imageUrl, changes, ocrResults) { | |
| const logEntry = document.createElement('div'); | |
| logEntry.className = 'log-entry'; | |
| const timestamp = new Date().toLocaleString(); | |
| logEntry.innerHTML = ` | |
| <div class="screenshot-container"> | |
| <img class="screenshot" src="${imageUrl}" alt="Screenshot"> | |
| ${changes.map((change, index) => ` | |
| <div class="change-highlight" style=" | |
| left: ${change.x}%; | |
| top: ${change.y}%; | |
| width: ${change.width}%; | |
| height: ${change.height}%; | |
| "></div> | |
| `).join('')} | |
| </div> | |
| <div class="info-panel"> | |
| <div class="timestamp">📅 ${timestamp}</div> | |
| <div class="analysis"> | |
| <h4>Changes Detected</h4> | |
| <p>${changes.length} regions changed</p> | |
| ${ocrResults.length > 0 ? ` | |
| <h4>Text Content</h4> | |
| <div class="ocr-text"> | |
| ${ocrResults.map(result => | |
| `[Region ${Math.round(result.confidence)}% confidence]\n${result.text}` | |
| ).join('\n\n')} | |
| </div> | |
| ` : ''} | |
| </div> | |
| <div class="actions"> | |
| <a href="${imageUrl}" download="screenshot-${Date.now()}.jpg" | |
| class="download-btn"> | |
| 📸 Download Screenshot | |
| </a> | |
| </div> | |
| </div> | |
| `; | |
| this.elements.logContainer.insertBefore(logEntry, this.elements.logContainer.firstChild); | |
| } | |
| showError(message) { | |
| this.updateStatus(message, true); | |
| console.error(message); | |
| } | |
| async cleanup() { | |
| if (this.worker) { | |
| await this.worker.terminate(); | |
| } | |
| this.stop(); | |
| } | |
| } | |
| // Initialize the application | |
| const analyzer = new ScreenAnalyzer(); | |
| window.addEventListener('beforeunload', () => analyzer.cleanup()); | |
| </script> | |
| </body> | |
| </html> |