Initial frontend commit
This commit is contained in:
822
index.html
Normal file
822
index.html
Normal file
@@ -0,0 +1,822 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Camera Text Recognition - API Integrated</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
||||
color: white;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.capture-mode {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
padding-bottom: 200px;
|
||||
}
|
||||
|
||||
.camera-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: black;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.crosshair {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
border: 3px solid #06b6d4;
|
||||
border-radius: 8px;
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.camera-hint {
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
color: #06b6d4;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.status-bar span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.status-dot.ready {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.result-box {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 2px solid #06b6d4;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.result-box label {
|
||||
display: block;
|
||||
color: #06b6d4;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.result-box p {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.result-box.error {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.result-box.error label {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.result-box.success {
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
.result-box.success label {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.result-box.not-found {
|
||||
border-color: #f97316;
|
||||
}
|
||||
|
||||
.result-box.not-found label {
|
||||
color: #f97316;
|
||||
}
|
||||
|
||||
.controls {
|
||||
background: #1e293b;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
max-width: 600px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 100;
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.language-select {
|
||||
background: #334155;
|
||||
border: 1px solid #475569;
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(135deg, #06b6d4 0%, #0284c7 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(6, 182, 212, 0.3);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #475569;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.playing-mode {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: black;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.video-title {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.video-meta {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
left: 20px;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
z-index: 10;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #dc2626 !important;
|
||||
padding: 12px 16px;
|
||||
border-radius: 50%;
|
||||
z-index: 10;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: #991b1b !important;
|
||||
}
|
||||
|
||||
.controls-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(to top, black, transparent);
|
||||
padding: 40px 20px 20px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
border: 3px solid #334155;
|
||||
border-top: 3px solid #06b6d4;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.confidence {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ef4444;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
right: 20px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border: 1px solid #334155;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.match-details {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- CAPTURE MODE -->
|
||||
<div id="captureMode" class="capture-mode">
|
||||
<div class="status-bar">
|
||||
<span><span class="status-dot" id="ocrDot"></span> OCR: <span id="ocrStatus">Loading...</span></span>
|
||||
<span><span class="status-dot" id="apiDot"></span> API: <span id="apiStatus">Checking...</span></span>
|
||||
</div>
|
||||
|
||||
<div class="camera-container">
|
||||
<video id="video" autoplay playsinline></video>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div class="crosshair"></div>
|
||||
<div class="camera-hint">Point camera at text to recognize</div>
|
||||
</div>
|
||||
|
||||
<div id="resultBox" class="result-box hidden">
|
||||
<label id="resultLabel">Recognized Text:</label>
|
||||
<p id="resultText"></p>
|
||||
<p id="resultStatus"></p>
|
||||
<div class="match-details" id="matchDetails"></div>
|
||||
<div class="confidence" id="confidence"></div>
|
||||
<div class="error-message" id="errorMessage"></div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<label for="language">Language:</label>
|
||||
<select id="language" class="language-select">
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="es">Español</option>
|
||||
<option value="it">Italiano</option>
|
||||
</select>
|
||||
<button id="permissionBtn" onclick="initCamera()" style="background: #3b82f6;">
|
||||
🔐 Request Camera Permission
|
||||
</button>
|
||||
<button id="captureBtn" onclick="captureAndRecognize()" disabled>
|
||||
📷 Capture & Recognize
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PLAYING MODE -->
|
||||
<div id="playingMode" class="playing-mode hidden">
|
||||
<div class="video-wrapper">
|
||||
<div class="video-title" id="videoTitle"></div>
|
||||
<div class="video-meta" id="videoMeta"></div>
|
||||
<button class="close-button" onclick="closeVideo()">✕</button>
|
||||
<video id="videoPlayer" class="video-player" autoplay muted controls></video>
|
||||
<div class="info-box" id="infoBox"></div>
|
||||
</div>
|
||||
|
||||
<div class="controls-bar">
|
||||
<button id="playPauseBtn" onclick="togglePlayPause()">▶️ Play</button>
|
||||
<button id="muteBtn" onclick="toggleMute()">🔊 Mute</button>
|
||||
<button onclick="closeVideo()">↩️ Back</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// CONFIGURATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
const API_BASE_URL = 'https://hotspots.fabio.ovh';
|
||||
|
||||
// Video base URL (configure based on where videos are hosted)
|
||||
const VIDEO_BASE_URL = ''; // Leave empty for local/relative paths
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// STATE
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
let currentLanguage = 'en';
|
||||
let currentContent = null;
|
||||
let isPlaying = false;
|
||||
let tesseractWorker = null;
|
||||
let ocrReady = false;
|
||||
let apiReady = false;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// API FUNCTIONS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async function checkApiHealth() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/health`);
|
||||
const data = await response.json();
|
||||
apiReady = data.status === 'healthy' && data.database_loaded;
|
||||
updateApiStatus(apiReady);
|
||||
console.log('✅ API Health:', data);
|
||||
return apiReady;
|
||||
} catch (err) {
|
||||
console.error('❌ API Health check failed:', err);
|
||||
apiReady = false;
|
||||
updateApiStatus(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function matchTextWithApi(text) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/match`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: text,
|
||||
language: currentLanguage
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('API Match result:', data);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('API Match error:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function updateApiStatus(ready) {
|
||||
const dot = document.getElementById('apiDot');
|
||||
const status = document.getElementById('apiStatus');
|
||||
if (ready) {
|
||||
dot.classList.add('ready');
|
||||
status.textContent = 'Ready';
|
||||
} else {
|
||||
dot.classList.remove('ready');
|
||||
status.textContent = 'Offline';
|
||||
}
|
||||
updateCaptureButton();
|
||||
}
|
||||
|
||||
function updateOcrStatus(ready) {
|
||||
const dot = document.getElementById('ocrDot');
|
||||
const status = document.getElementById('ocrStatus');
|
||||
if (ready) {
|
||||
dot.classList.add('ready');
|
||||
status.textContent = 'Ready';
|
||||
} else {
|
||||
dot.classList.remove('ready');
|
||||
status.textContent = 'Loading...';
|
||||
}
|
||||
updateCaptureButton();
|
||||
}
|
||||
|
||||
function updateCaptureButton() {
|
||||
const btn = document.getElementById('captureBtn');
|
||||
btn.disabled = !(ocrReady && apiReady);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// OCR FUNCTIONS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async function initOCR() {
|
||||
try {
|
||||
console.log('=== TESSERACT INITIALIZATION START ===');
|
||||
tesseractWorker = await Tesseract.createWorker();
|
||||
await tesseractWorker.loadLanguage('eng');
|
||||
await tesseractWorker.initialize('eng');
|
||||
ocrReady = true;
|
||||
updateOcrStatus(true);
|
||||
console.log('✅ TESSERACT FULLY READY');
|
||||
} catch (err) {
|
||||
console.error('❌ OCR initialization failed:', err);
|
||||
ocrReady = false;
|
||||
updateOcrStatus(false);
|
||||
showError('OCR initialization failed: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// CAMERA FUNCTIONS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async function initCamera() {
|
||||
try {
|
||||
console.log('=== CAMERA INITIALIZATION ===');
|
||||
const constraints = {
|
||||
video: {
|
||||
facingMode: 'environment',
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 }
|
||||
}
|
||||
};
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
const video = document.getElementById('video');
|
||||
video.srcObject = stream;
|
||||
|
||||
video.onloadedmetadata = () => {
|
||||
video.play();
|
||||
console.log('✅ Camera ready');
|
||||
};
|
||||
|
||||
// Hide permission button after success
|
||||
document.getElementById('permissionBtn').style.display = 'none';
|
||||
|
||||
} catch (err) {
|
||||
console.error('Camera error:', err);
|
||||
if (err.name === 'NotAllowedError') {
|
||||
showError('❌ Camera permission denied. Please allow camera access.');
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
showError('❌ No camera found on this device.');
|
||||
} else {
|
||||
showError('❌ Camera access failed: ' + err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// CAPTURE & RECOGNIZE
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async function captureAndRecognize() {
|
||||
if (!tesseractWorker || !apiReady) {
|
||||
showError('OCR or API not ready. Please wait...');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('captureBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Recognizing...';
|
||||
|
||||
currentLanguage = document.getElementById('language').value;
|
||||
|
||||
try {
|
||||
const video = document.getElementById('video');
|
||||
const canvas = document.getElementById('canvas');
|
||||
|
||||
if (video.readyState !== 4 || video.videoWidth === 0) {
|
||||
showError('Camera not ready. Try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
ctx.drawImage(video, 0, 0);
|
||||
|
||||
// OCR
|
||||
btn.innerHTML = '<span class="spinner"></span> Reading text...';
|
||||
const result = await tesseractWorker.recognize(canvas);
|
||||
const { data: { text, confidence } } = result;
|
||||
const recognizedText = text.trim();
|
||||
|
||||
console.log('Recognized:', recognizedText, 'Confidence:', confidence);
|
||||
|
||||
if (!recognizedText) {
|
||||
showNoTextFound();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show recognized text
|
||||
showRecognizedText(recognizedText, confidence);
|
||||
|
||||
// Match with API
|
||||
btn.innerHTML = '<span class="spinner"></span> Matching...';
|
||||
const matchResult = await matchTextWithApi(recognizedText);
|
||||
|
||||
if (matchResult.found) {
|
||||
showMatchFound(matchResult, recognizedText, confidence);
|
||||
// Auto-load content after short delay
|
||||
setTimeout(() => loadContent(matchResult), 1500);
|
||||
} else {
|
||||
showContentNotFound(recognizedText);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Recognition error:', err);
|
||||
showError('Recognition failed: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '📷 Capture & Recognize';
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// UI FUNCTIONS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function showRecognizedText(text, confidence) {
|
||||
const box = document.getElementById('resultBox');
|
||||
const label = document.getElementById('resultLabel');
|
||||
const resultText = document.getElementById('resultText');
|
||||
const confidenceEl = document.getElementById('confidence');
|
||||
const errorMsg = document.getElementById('errorMessage');
|
||||
const matchDetails = document.getElementById('matchDetails');
|
||||
|
||||
box.classList.remove('hidden', 'error', 'not-found', 'success');
|
||||
label.textContent = '🔍 Text Recognized';
|
||||
resultText.textContent = text;
|
||||
confidenceEl.textContent = `OCR Confidence: ${confidence.toFixed(1)}%`;
|
||||
matchDetails.textContent = 'Searching database...';
|
||||
errorMsg.textContent = '';
|
||||
}
|
||||
|
||||
function showMatchFound(match, recognizedText, ocrConfidence) {
|
||||
const box = document.getElementById('resultBox');
|
||||
const label = document.getElementById('resultLabel');
|
||||
const resultText = document.getElementById('resultText');
|
||||
const matchDetails = document.getElementById('matchDetails');
|
||||
const confidenceEl = document.getElementById('confidence');
|
||||
|
||||
box.classList.remove('error', 'not-found');
|
||||
box.classList.add('success');
|
||||
label.textContent = '✅ Match Found!';
|
||||
resultText.textContent = match.name;
|
||||
matchDetails.textContent = `Hotspot ID: ${match.hotspot_id}`;
|
||||
confidenceEl.textContent = `OCR: ${ocrConfidence.toFixed(1)}% | Match: ${(match.confidence * 100).toFixed(0)}%`;
|
||||
|
||||
currentContent = match;
|
||||
}
|
||||
|
||||
function showNoTextFound() {
|
||||
const box = document.getElementById('resultBox');
|
||||
const label = document.getElementById('resultLabel');
|
||||
const resultText = document.getElementById('resultText');
|
||||
const errorMsg = document.getElementById('errorMessage');
|
||||
const matchDetails = document.getElementById('matchDetails');
|
||||
|
||||
box.classList.remove('hidden', 'success', 'not-found');
|
||||
box.classList.add('error');
|
||||
label.textContent = '⚠️ No Text Detected';
|
||||
resultText.textContent = '';
|
||||
matchDetails.textContent = '';
|
||||
errorMsg.textContent = 'No readable text found. Try pointing at clear, well-lit text.';
|
||||
}
|
||||
|
||||
function showContentNotFound(recognizedText) {
|
||||
const box = document.getElementById('resultBox');
|
||||
const label = document.getElementById('resultLabel');
|
||||
const resultText = document.getElementById('resultText');
|
||||
const resultStatus = document.getElementById('resultStatus');
|
||||
const errorMsg = document.getElementById('errorMessage');
|
||||
const matchDetails = document.getElementById('matchDetails');
|
||||
|
||||
box.classList.remove('hidden', 'success', 'error');
|
||||
box.classList.add('not-found');
|
||||
label.textContent = '❌ No Match Found';
|
||||
resultText.textContent = recognizedText;
|
||||
resultStatus.textContent = '';
|
||||
matchDetails.textContent = 'Text does not match any hotspot in database';
|
||||
errorMsg.textContent = 'Try: Brussels, Paris, Berlin, Rome...';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const box = document.getElementById('resultBox');
|
||||
const label = document.getElementById('resultLabel');
|
||||
const resultText = document.getElementById('resultText');
|
||||
const errorMsg = document.getElementById('errorMessage');
|
||||
const matchDetails = document.getElementById('matchDetails');
|
||||
|
||||
box.classList.remove('hidden', 'success', 'not-found');
|
||||
box.classList.add('error');
|
||||
label.textContent = '⚠️ Error';
|
||||
resultText.textContent = '';
|
||||
matchDetails.textContent = '';
|
||||
errorMsg.textContent = message;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// VIDEO PLAYBACK
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function loadContent(match) {
|
||||
const videoPlayer = document.getElementById('videoPlayer');
|
||||
const videoTitle = document.getElementById('videoTitle');
|
||||
const videoMeta = document.getElementById('videoMeta');
|
||||
const infoBox = document.getElementById('infoBox');
|
||||
|
||||
videoTitle.textContent = match.name;
|
||||
videoMeta.textContent = `Hotspot ID: ${match.hotspot_id}`;
|
||||
|
||||
infoBox.innerHTML = `
|
||||
<strong>${match.name}</strong><br>
|
||||
ID: ${match.hotspot_id}<br>
|
||||
Match: ${(match.confidence * 100).toFixed(0)}%<br>
|
||||
${match.video_ids ? `Videos: ${match.video_ids.join(', ')}` : 'No video mapped'}
|
||||
`;
|
||||
|
||||
// Try to load video if video_ids exist
|
||||
if (match.video_ids && match.video_ids.length > 0) {
|
||||
const videoId = match.video_ids[0];
|
||||
const videoUrl = VIDEO_BASE_URL ? `${VIDEO_BASE_URL}/${videoId}.mp4` : `${videoId}.mp4`;
|
||||
|
||||
videoPlayer.src = videoUrl;
|
||||
videoPlayer.onerror = () => {
|
||||
// Fallback to sample video
|
||||
console.log('Video not found, using sample');
|
||||
videoPlayer.src = 'https://commondatastorage.googleapis.com/gtv-videos-library/sample/BigBuckBunny.mp4';
|
||||
};
|
||||
} else {
|
||||
// Use sample video
|
||||
videoPlayer.src = 'https://commondatastorage.googleapis.com/gtv-videos-library/sample/BigBuckBunny.mp4';
|
||||
}
|
||||
|
||||
videoPlayer.play();
|
||||
isPlaying = true;
|
||||
updatePlayPauseButton();
|
||||
|
||||
document.getElementById('captureMode').classList.add('hidden');
|
||||
document.getElementById('playingMode').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeVideo() {
|
||||
document.getElementById('videoPlayer').pause();
|
||||
document.getElementById('playingMode').classList.add('hidden');
|
||||
document.getElementById('captureMode').classList.remove('hidden');
|
||||
document.getElementById('resultBox').classList.add('hidden');
|
||||
currentContent = null;
|
||||
isPlaying = false;
|
||||
}
|
||||
|
||||
function togglePlayPause() {
|
||||
const video = document.getElementById('videoPlayer');
|
||||
if (isPlaying) {
|
||||
video.pause();
|
||||
} else {
|
||||
video.play();
|
||||
}
|
||||
isPlaying = !isPlaying;
|
||||
updatePlayPauseButton();
|
||||
}
|
||||
|
||||
function updatePlayPauseButton() {
|
||||
const btn = document.getElementById('playPauseBtn');
|
||||
btn.textContent = isPlaying ? '⏸️ Pause' : '▶️ Play';
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
const video = document.getElementById('videoPlayer');
|
||||
const btn = document.getElementById('muteBtn');
|
||||
video.muted = !video.muted;
|
||||
btn.textContent = video.muted ? '🔇 Unmute' : '🔊 Mute';
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// INITIALIZATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
console.log('=== APP INITIALIZATION ===');
|
||||
|
||||
// Language change handler
|
||||
document.getElementById('language').addEventListener('change', (e) => {
|
||||
currentLanguage = e.target.value;
|
||||
});
|
||||
|
||||
// Initialize in parallel
|
||||
await Promise.all([
|
||||
initOCR(),
|
||||
checkApiHealth()
|
||||
]);
|
||||
|
||||
// Auto-init camera on mobile
|
||||
if (/Android|iPhone|iPad/i.test(navigator.userAgent)) {
|
||||
initCamera();
|
||||
}
|
||||
|
||||
console.log('=== APP READY ===');
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
window.addEventListener('beforeunload', async () => {
|
||||
if (tesseractWorker) {
|
||||
await tesseractWorker.terminate();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user