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/[email protected]/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> |