unknown
Improved UI
c96728f
Raw
History Blame Contribute Delete
31.1 kB
{% extends 'base.html' %}
{% block title %}Live Detection — Constellar{% endblock %}
{% block extra_css %}
<style>
/* ── status bar ── */
.status-bar {
display: flex; align-items: center; justify-content: center; gap: .625rem;
padding: .75rem 1.25rem;
border-radius: 10px;
background: var(--surface);
border: 1px solid var(--border);
font-size: .8125rem; font-weight: 500;
color: var(--muted);
transition: all .25s;
}
.status-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--accent);
flex-shrink: 0;
}
.status-bar.status-error { background: rgba(239,68,68,.08); border-color: rgba(239,68,68,.25); color: #fca5a5; }
.status-bar.status-error .status-dot { background: #ef4444; }
.status-bar.status-success { background: rgba(34,197,94,.08); border-color: rgba(34,197,94,.25); color: #86efac; }
.status-bar.status-success .status-dot { background: #22c55e; }
.status-bar.status-info { background: rgba(99,102,241,.07); border-color: rgba(99,102,241,.22); color: #a5b4fc; }
.status-bar.status-info .status-dot { background: var(--accent); animation: dotpulse 1.4s ease-in-out infinite; }
@keyframes dotpulse { 0%,100%{opacity:1} 50%{opacity:.35} }
/* ── video container ── */
.video-shell {
background: rgba(0,0,0,.45);
border: 1px solid var(--border);
border-radius: 14px;
overflow: hidden;
}
.video-container {
position: relative;
width: 100%; height: 0;
padding-bottom: 62%;
}
@media (min-width: 768px) { .video-container { padding-bottom: 56.25%; } }
.video-container video,
.video-container img,
.video-container canvas {
position: absolute; top: 0; left: 0;
width: 100%; height: 100%;
object-fit: cover;
}
/* ── control buttons ── */
.ctl-btn {
font-family: 'Inter', sans-serif;
font-size: .875rem; font-weight: 600;
display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
padding: .7rem 1.5rem;
min-height: 44px;
border-radius: 8px; border: none; cursor: pointer;
transition: all .18s;
touch-action: manipulation;
}
.ctl-btn.primary { background: var(--accent); color: #fff; }
.ctl-btn.primary:hover:not(:disabled) { background: #818cf8; box-shadow: 0 0 20px rgba(99,102,241,.35); transform: translateY(-1px); }
.ctl-btn.outline {
background: rgba(255,255,255,.04);
border: 1px solid var(--border);
color: var(--muted);
}
.ctl-btn.outline:hover:not(:disabled) { color: #fff; border-color: rgba(255,255,255,.18); background: rgba(255,255,255,.07); }
.ctl-btn:disabled { opacity: .4; cursor: not-allowed; transform: none !important; box-shadow: none !important; }
/* ── troubleshoot steps ── */
.ts-step { display: flex; gap: .875rem; align-items: flex-start; }
.ts-num {
width: 22px; height: 22px; flex-shrink: 0;
border-radius: 6px;
background: rgba(99,102,241,.15);
border: 1px solid rgba(99,102,241,.3);
color: #a5b4fc;
font-size: .6875rem; font-weight: 700;
display: flex; align-items: center; justify-content: center;
margin-top: 2px;
}
.ts-step h4 { font-size: .8125rem; font-weight: 600; margin: 0 0 .25rem; color: var(--text); }
.ts-step p { font-size: .75rem; color: var(--muted); line-height: 1.6; margin: 0; }
.ts-step code {
background: rgba(255,255,255,.06);
padding: .1rem .375rem; border-radius: 4px;
font-size: .6875rem;
}
/* ── fullscreen ── */
.fullscreen-controls {
position: fixed; bottom: 20px; left: 50%;
transform: translateX(-50%);
z-index: 1000;
display: flex; gap: .5rem;
background: rgba(6,13,31,.9);
border: 1px solid var(--border);
padding: .625rem;
border-radius: 12px;
backdrop-filter: blur(16px);
}
.fullscreen-controls .ctl-btn { padding: .5rem 1rem; font-size: .78125rem; min-height: 38px; }
.fullscreen-active {
position: fixed !important;
top: 0 !important; left: 0 !important;
width: 100vw !important; height: 100vh !important;
z-index: 999 !important;
background: black !important;
padding-bottom: 0 !important;
border-radius: 0 !important;
}
</style>
{% endblock %}
{% block content %}
<div class="section" style="max-width:900px; padding-top:3.5rem; padding-bottom:2rem;">
<!-- ════ HEADER ════ -->
<div style="text-align:center; margin-bottom:2rem;">
<div class="tag" style="margin-bottom:1.25rem;">Real-Time Analysis</div>
<h1 style="font-family:'Orbitron',monospace; font-size:clamp(1.5rem,3.5vw,2.25rem); font-weight:900; margin:0 0 .875rem; letter-spacing:-.01em;">
Live Constellation Detection
</h1>
<p style="font-size:.9375rem; color:var(--muted); line-height:1.7; max-width:520px; margin:0 auto;">
Point your camera at the night sky — detected constellations are outlined and labeled directly on the feed.
</p>
</div>
<!-- ════ STATUS ════ -->
<div id="status" class="status-bar status-info" style="margin-bottom:1rem;">
<div class="status-content" style="display:flex; align-items:center; gap:.625rem;">
<div class="status-dot status-icon"></div>
<span>Press "Start Camera" to begin</span>
</div>
</div>
<!-- ════ VIDEO ════ -->
<div class="video-shell" style="margin-bottom:1rem;">
<div id="videoContainer" class="video-container">
<video id="video" autoplay playsinline muted style="display:none;"></video>
<canvas id="canvas" width="640" height="480" class="hidden"></canvas>
<img id="result" style="display:none;" alt="Detection results" />
<!-- placeholder -->
<div id="placeholder" style="position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; padding:1.5rem; text-align:center;">
<div style="width:56px; height:56px; border-radius:14px; background:rgba(99,102,241,.1); border:1px solid rgba(99,102,241,.22); display:flex; align-items:center; justify-content:center; margin-bottom:1rem;">
<svg width="24" height="24" fill="none" stroke="#a5b4fc" stroke-width="1.6" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"/>
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Z"/>
</svg>
</div>
<h3 style="font-size:.9375rem; font-weight:600; margin:0 0 .375rem; color:var(--text);">Camera preview</h3>
<p style="font-size:.78125rem; color:var(--muted); max-width:280px; margin:0; line-height:1.6;">Your camera feed will appear here. Point toward the night sky for best results.</p>
</div>
<!-- fullscreen toggle -->
<button id="fullscreenToggle" style="display:none; position:absolute; top:12px; right:12px; background:rgba(6,13,31,.7); border:1px solid var(--border); color:#fff; padding:.5rem; border-radius:8px; cursor:pointer; backdrop-filter:blur(8px);">
<svg class="w-5 h-5" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"/>
</svg>
</button>
</div>
</div>
<!-- ════ CONTROLS ════ -->
<div class="glass-card" style="padding:1.5rem; margin-bottom:1rem;">
<div style="display:flex; flex-wrap:wrap; gap:.625rem; justify-content:center;">
<button id="startCamera" class="ctl-btn primary">
<svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" d="M15 10l4.553-2.276A1 1 0 0 1 21 8.618v6.764a1 1 0 0 1-1.447.894L15 14M5 18h8a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2z"/></svg>
Start Camera
</button>
<button id="switchCamera" disabled class="ctl-btn outline">
<svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 0 0 4.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 0 1-15.357-2m15.357 2H15"/></svg>
Switch Camera
</button>
<button id="startDetection" disabled class="ctl-btn primary">
<svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path stroke-linecap="round" d="m21 21-4.35-4.35"/></svg>
Start Detection
</button>
<button id="stopDetection" disabled class="ctl-btn outline">
<svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg>
Stop
</button>
<button id="showTroubleshooting" class="ctl-btn outline">
<svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"/></svg>
Help
</button>
</div>
<p style="text-align:center; font-size:.6875rem; color:rgba(255,255,255,.2); margin:1rem 0 0;">
Shortcuts: <kbd style="background:rgba(255,255,255,.06); padding:.1rem .375rem; border-radius:4px;">Space</kbd> detect ·
<kbd style="background:rgba(255,255,255,.06); padding:.1rem .375rem; border-radius:4px;">F</kbd> fullscreen ·
<kbd style="background:rgba(255,255,255,.06); padding:.1rem .375rem; border-radius:4px;">C</kbd> switch camera
</p>
</div>
<!-- ════ TROUBLESHOOTING ════ -->
<div id="troubleshooting" class="glass-card" style="padding:1.75rem; margin-bottom:1rem; display:none;">
<h3 style="font-size:.9375rem; font-weight:700; margin:0 0 1.25rem; color:var(--text);">Camera Troubleshooting</h3>
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:1rem 2rem; margin-bottom:1.25rem;">
<div class="ts-step">
<div class="ts-num">1</div>
<div>
<h4>HTTPS required</h4>
<p>Camera access needs <code>https://</code> or <code>localhost</code>. Check your address bar.</p>
</div>
</div>
<div class="ts-step">
<div class="ts-num">2</div>
<div>
<h4>Camera permission</h4>
<p>Allow camera access when prompted, or reset it in your browser's site settings.</p>
</div>
</div>
<div class="ts-step">
<div class="ts-num">3</div>
<div>
<h4>Camera in use</h4>
<p>Close other apps that may be holding the camera (video calls, camera app).</p>
</div>
</div>
<div class="ts-step">
<div class="ts-num">4</div>
<div>
<h4>iPhone / iPad</h4>
<p>Use Safari on iOS for the most reliable camera support.</p>
</div>
</div>
</div>
<div style="background:rgba(255,255,255,.03); border:1px solid var(--border); border-radius:8px; padding:.875rem 1rem; font-size:.71875rem; color:var(--muted); line-height:1.8; word-break:break-all;">
<div><span style="color:#a5b4fc; font-weight:600;">URL:</span> <code id="currentUrl"></code></div>
<div><span style="color:#a5b4fc; font-weight:600;">Device:</span> <code id="deviceInfo"></code></div>
</div>
</div>
<!-- ════ TIPS ════ -->
<div class="glass-card" style="padding:1.5rem;">
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:1.25rem;">
<div style="display:flex; gap:.75rem; align-items:flex-start;">
<svg width="16" height="16" fill="none" stroke="#a5b4fc" stroke-width="1.8" viewBox="0 0 24 24" style="flex-shrink:0; margin-top:2px;"><path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"/></svg>
<div>
<h3 style="font-size:.8125rem; font-weight:600; margin:0 0 .25rem;">Dark conditions</h3>
<p style="font-size:.75rem; color:var(--muted); line-height:1.6; margin:0;">Use at night, away from bright city lights.</p>
</div>
</div>
<div style="display:flex; gap:.75rem; align-items:flex-start;">
<svg width="16" height="16" fill="none" stroke="#a5b4fc" stroke-width="1.8" viewBox="0 0 24 24" style="flex-shrink:0; margin-top:2px;"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3"/></svg>
<div>
<h3 style="font-size:.8125rem; font-weight:600; margin:0 0 .25rem;">Hold steady</h3>
<p style="font-size:.75rem; color:var(--muted); line-height:1.6; margin:0;">A stable device gives clearer frames for detection.</p>
</div>
</div>
<div style="display:flex; gap:.75rem; align-items:flex-start;">
<svg width="16" height="16" fill="none" stroke="#a5b4fc" stroke-width="1.8" viewBox="0 0 24 24" style="flex-shrink:0; margin-top:2px;"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/></svg>
<div>
<h3 style="font-size:.8125rem; font-weight:600; margin:0 0 .25rem;">Unobstructed view</h3>
<p style="font-size:.75rem; color:var(--muted); line-height:1.6; margin:0;">Avoid clouds, trees, and buildings in the frame.</p>
</div>
</div>
</div>
</div>
</div>
<!-- ════ FULLSCREEN CONTROLS ════ -->
<div id="fullscreenControls" class="fullscreen-controls" style="display:none;">
<button id="fullscreenStartDetection" class="ctl-btn primary">Detect</button>
<button id="fullscreenStopDetection" class="ctl-btn outline">Stop</button>
<button id="fullscreenSwitchCamera" class="ctl-btn outline">Switch</button>
<button id="exitFullscreen" class="ctl-btn outline">Exit</button>
</div>
{% endblock %}
{% block extra_js %}
<script>
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const resultImg = document.getElementById('result');
const placeholder = document.getElementById('placeholder');
const videoContainer = document.getElementById('videoContainer');
const startCameraBtn = document.getElementById('startCamera');
const switchCameraBtn = document.getElementById('switchCamera');
const startDetectionBtn = document.getElementById('startDetection');
const stopDetectionBtn = document.getElementById('stopDetection');
const showTroubleshootingBtn = document.getElementById('showTroubleshooting');
const fullscreenToggle = document.getElementById('fullscreenToggle');
const fullscreenControls = document.getElementById('fullscreenControls');
const statusDiv = document.getElementById('status');
const troubleshootingDiv = document.getElementById('troubleshooting');
const ctx = canvas.getContext('2d');
let streaming = false;
let cameraStream = null;
let detectionActive = false;
let currentFacingMode = 'environment';
let availableCameras = [];
let currentCameraIndex = 0;
let isFullscreen = false;
function isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
function updateStatus(message, type = 'info') {
const statusText = statusDiv.querySelector('.status-content span');
statusText.textContent = message;
statusDiv.className = 'status-bar';
statusDiv.classList.add(
type === 'error' ? 'status-error' :
type === 'success' ? 'status-success' : 'status-info'
);
statusDiv.style.marginBottom = '1rem';
}
async function getAvailableCameras() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
availableCameras = devices.filter(d => d.kind === 'videoinput');
if (availableCameras.length > 1) switchCameraBtn.style.display = 'inline-flex';
return availableCameras;
} catch (err) {
console.error('Error getting camera list:', err);
return [];
}
}
function checkBrowserSupport() {
const isSecureContext = window.isSecureContext || location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1';
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
updateStatus('Camera access not supported in this browser. Please use a modern browser.', 'error');
startCameraBtn.disabled = true;
showTroubleshootingInfo();
return false;
}
if (!isSecureContext) {
updateStatus('Camera requires HTTPS or localhost.', 'error');
startCameraBtn.disabled = true;
showTroubleshootingInfo();
return false;
}
return true;
}
function showTroubleshootingInfo() {
troubleshootingDiv.style.display = 'block';
document.getElementById('currentUrl').textContent = window.location.href;
document.getElementById('deviceInfo').textContent =
`${navigator.platform} | Mobile: ${isMobile() ? 'Yes' : 'No'} | Touch: ${'ontouchstart' in window ? 'Yes' : 'No'}`;
}
function getCameraConstraints() {
const constraints = {
video: {
width: { ideal: isMobile() ? 720 : 1280 },
height: { ideal: isMobile() ? 960 : 720 },
}
};
if (availableCameras.length > 0) {
constraints.video.deviceId = { exact: availableCameras[currentCameraIndex].deviceId };
} else {
constraints.video.facingMode = currentFacingMode;
}
return constraints;
}
async function startCamera() {
try {
updateStatus('Starting camera…', 'info');
await getAvailableCameras();
const constraints = getCameraConstraints();
if (cameraStream) cameraStream.getTracks().forEach(t => t.stop());
cameraStream = await navigator.mediaDevices.getUserMedia(constraints);
video.srcObject = cameraStream;
video.onloadedmetadata = () => {
video.play().then(() => {
streaming = true;
video.style.display = 'block';
placeholder.style.display = 'none';
fullscreenToggle.style.display = 'block';
startCameraBtn.disabled = true;
startDetectionBtn.disabled = false;
switchCameraBtn.disabled = false;
const cameraName = availableCameras.length > 0 ?
(availableCameras[currentCameraIndex].label || `Camera ${currentCameraIndex + 1}`) :
(currentFacingMode === 'environment' ? 'Back camera' : 'Front camera');
updateStatus(`${cameraName} active. Point at the sky and press Start Detection.`, 'success');
}).catch(err => {
updateStatus(`Error playing video: ${err.message}`, 'error');
});
};
} catch (err) {
console.error("Error accessing webcam:", err);
let errorMessage = 'Camera access failed: ';
if (err.name === 'NotAllowedError') {
errorMessage += 'permission denied. Allow camera access and try again.';
} else if (err.name === 'NotFoundError') {
errorMessage += 'no camera found on this device.';
} else if (err.name === 'NotReadableError') {
errorMessage += 'camera is used by another app. Close other camera apps.';
} else if (err.name === 'OverconstrainedError') {
errorMessage += 'trying fallback settings…';
setTimeout(() => startCameraFallback(), 1000);
} else {
errorMessage += err.message;
}
updateStatus(errorMessage, 'error');
}
}
async function startCameraFallback() {
try {
updateStatus('Trying basic camera access…', 'info');
if (cameraStream) cameraStream.getTracks().forEach(t => t.stop());
cameraStream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = cameraStream;
video.onloadedmetadata = () => {
video.play().then(() => {
streaming = true;
video.style.display = 'block';
placeholder.style.display = 'none';
fullscreenToggle.style.display = 'block';
startCameraBtn.disabled = true;
startDetectionBtn.disabled = false;
updateStatus('Camera started with basic settings. Ready for detection.', 'success');
});
};
} catch (err) {
updateStatus('Unable to access any camera. Check permissions and try again.', 'error');
showTroubleshootingInfo();
}
}
async function switchCamera() {
if (availableCameras.length <= 1) {
currentFacingMode = currentFacingMode === 'environment' ? 'user' : 'environment';
} else {
currentCameraIndex = (currentCameraIndex + 1) % availableCameras.length;
}
updateStatus('Switching camera…', 'info');
await startCamera();
}
function detectLoop() {
if (!detectionActive || !streaming) return;
try {
const videoWidth = video.videoWidth || 640;
const videoHeight = video.videoHeight || 480;
canvas.width = videoWidth;
canvas.height = videoHeight;
ctx.drawImage(video, 0, 0, videoWidth, videoHeight);
canvas.toBlob(blob => {
if (!blob || !detectionActive) return;
const formData = new FormData();
formData.append('frame', blob, 'frame.jpg');
fetch('/detect/process/', { method: 'POST', body: formData })
.then(response => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.blob();
})
.then(imageBlob => {
if (detectionActive) {
resultImg.src = URL.createObjectURL(imageBlob);
resultImg.style.display = 'block';
video.style.display = 'none';
setTimeout(() => {
if (detectionActive) requestAnimationFrame(detectLoop);
}, 500);
}
})
.catch(err => {
console.error('Detection error:', err);
updateStatus(`Detection error: ${err.message}`, 'error');
stopDetection();
});
}, 'image/jpeg', 0.7);
} catch (err) {
console.error('Canvas error:', err);
updateStatus(`Canvas error: ${err.message}`, 'error');
stopDetection();
}
}
function startDetection() {
if (!streaming) {
updateStatus('Start the camera first.', 'error');
return;
}
detectionActive = true;
startDetectionBtn.disabled = true;
stopDetectionBtn.disabled = false;
document.getElementById('fullscreenStartDetection').disabled = true;
document.getElementById('fullscreenStopDetection').disabled = false;
updateStatus('Scanning the sky for constellations…', 'info');
detectLoop();
}
function stopDetection() {
detectionActive = false;
startDetectionBtn.disabled = false;
stopDetectionBtn.disabled = true;
document.getElementById('fullscreenStartDetection').disabled = false;
document.getElementById('fullscreenStopDetection').disabled = true;
resultImg.style.display = 'none';
video.style.display = 'block';
updateStatus('Detection paused. Camera is still active.', 'info');
}
function stopCamera() {
if (cameraStream) {
cameraStream.getTracks().forEach(t => t.stop());
cameraStream = null;
}
streaming = false;
detectionActive = false;
video.style.display = 'none';
resultImg.style.display = 'none';
placeholder.style.display = 'flex';
fullscreenToggle.style.display = 'none';
startCameraBtn.disabled = false;
startDetectionBtn.disabled = true;
stopDetectionBtn.disabled = true;
switchCameraBtn.disabled = true;
updateStatus('Camera stopped.', 'info');
}
function toggleFullscreen() {
isFullscreen ? exitFullscreen() : enterFullscreen();
}
function enterFullscreen() {
videoContainer.classList.add('fullscreen-active');
fullscreenControls.style.display = 'flex';
fullscreenToggle.innerHTML = `
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>`;
isFullscreen = true;
if (screen.orientation && screen.orientation.lock) {
screen.orientation.lock('landscape').catch(() => {});
}
}
function exitFullscreen() {
videoContainer.classList.remove('fullscreen-active');
fullscreenControls.style.display = 'none';
fullscreenToggle.innerHTML = `
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"/>
</svg>`;
isFullscreen = false;
if (screen.orientation && screen.orientation.unlock) {
screen.orientation.unlock();
}
}
startCameraBtn.addEventListener('click', startCamera);
switchCameraBtn.addEventListener('click', switchCamera);
startDetectionBtn.addEventListener('click', startDetection);
stopDetectionBtn.addEventListener('click', stopDetection);
fullscreenToggle.addEventListener('click', toggleFullscreen);
document.getElementById('fullscreenStartDetection').addEventListener('click', startDetection);
document.getElementById('fullscreenStopDetection').addEventListener('click', stopDetection);
document.getElementById('fullscreenSwitchCamera').addEventListener('click', switchCamera);
document.getElementById('exitFullscreen').addEventListener('click', exitFullscreen);
showTroubleshootingBtn.addEventListener('click', () => {
const isVisible = troubleshootingDiv.style.display !== 'none';
troubleshootingDiv.style.display = isVisible ? 'none' : 'block';
if (!isVisible) showTroubleshootingInfo();
});
if (isMobile()) {
let lastTouchEnd = 0;
document.addEventListener('touchend', function (event) {
const now = Date.now();
if (now - lastTouchEnd <= 300) event.preventDefault();
lastTouchEnd = now;
}, false);
window.addEventListener('orientationchange', () => {
setTimeout(() => {
if (streaming && video.videoWidth && video.videoHeight) {
video.style.width = '100%';
video.style.height = '100%';
}
}, 500);
});
document.addEventListener('visibilitychange', () => {
if (document.hidden && detectionActive) {
stopDetection();
updateStatus('Detection paused — app in background.', 'info');
}
});
}
document.addEventListener('keydown', (e) => {
if (e.key === 'f' || e.key === 'F') {
if (streaming) toggleFullscreen();
} else if (e.key === ' ') {
e.preventDefault();
if (streaming && !detectionActive) startDetection();
else if (detectionActive) stopDetection();
} else if (e.key === 'c' || e.key === 'C') {
if (streaming && availableCameras.length > 1) switchCamera();
}
});
window.addEventListener('beforeunload', stopCamera);
if (checkBrowserSupport()) {
if (isMobile()) {
updateStatus('Mobile device detected. Tap "Start Camera" to begin.', 'info');
}
getAvailableCameras();
}
</script>
{% endblock %}