268 lines
9.6 KiB
JavaScript
268 lines
9.6 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function() {
|
|
let html5QrCode;
|
|
const btnScanBarcode = document.getElementById('btnScanBarcode');
|
|
const btnTakePhoto = document.getElementById('btnTakePhoto');
|
|
const reader = document.getElementById('reader');
|
|
const aiLoading = document.getElementById('aiLoading');
|
|
|
|
const pName = document.getElementById('p_name');
|
|
const pCategory = document.getElementById('p_category');
|
|
const pQuantity = document.getElementById('p_quantity');
|
|
const pExpiration = document.getElementById('p_expiration');
|
|
|
|
function checkHttps() {
|
|
if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
|
|
alert("Camera access requires HTTPS. Please ensure you are using a secure connection.");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function stopScanner() {
|
|
return new Promise((resolve) => {
|
|
console.log("Stopping scanner...");
|
|
cleanupUI();
|
|
if (html5QrCode && html5QrCode.isScanning) {
|
|
html5QrCode.stop().then(() => {
|
|
console.log("Scanner stopped.");
|
|
reader.style.display = 'none';
|
|
resolve();
|
|
}).catch(err => {
|
|
console.error("Failed to stop scanner", err);
|
|
reader.style.display = 'none';
|
|
resolve();
|
|
});
|
|
} else {
|
|
console.log("Scanner was not active.");
|
|
reader.style.display = 'none';
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
function cleanupUI() {
|
|
const capBtn = document.getElementById('btnCaptureFrame');
|
|
if (capBtn) capBtn.remove();
|
|
const stopBtn = document.getElementById('btnStopScanner');
|
|
if (stopBtn) stopBtn.remove();
|
|
const flash = document.querySelector('.shutter-flash');
|
|
if (flash) flash.classList.remove('active');
|
|
reader.classList.remove('camera-mode');
|
|
}
|
|
|
|
function addStopButton() {
|
|
if (!document.getElementById('btnStopScanner')) {
|
|
const stopBtn = document.createElement('button');
|
|
stopBtn.id = 'btnStopScanner';
|
|
stopBtn.innerText = "Stop Camera";
|
|
stopBtn.className = "btn btn-danger btn-sm w-100 mt-2";
|
|
stopBtn.onclick = stopScanner;
|
|
reader.after(stopBtn);
|
|
}
|
|
}
|
|
|
|
function triggerFlash() {
|
|
let flash = document.querySelector('.shutter-flash');
|
|
if (!flash) {
|
|
flash = document.createElement('div');
|
|
flash.className = 'shutter-flash';
|
|
reader.appendChild(flash);
|
|
}
|
|
flash.classList.add('active');
|
|
setTimeout(() => flash.classList.remove('active'), 150);
|
|
}
|
|
|
|
btnScanBarcode.addEventListener('click', function() {
|
|
if (!checkHttps()) return;
|
|
|
|
stopScanner().then(() => {
|
|
reader.style.display = 'block';
|
|
if (!html5QrCode) {
|
|
html5QrCode = new Html5Qrcode("reader");
|
|
}
|
|
|
|
addStopButton();
|
|
|
|
const config = {
|
|
fps: 10,
|
|
qrbox: (viewfinderWidth, viewfinderHeight) => {
|
|
return { width: viewfinderWidth * 0.8, height: viewfinderHeight * 0.4 };
|
|
}
|
|
};
|
|
|
|
html5QrCode.start(
|
|
{ facingMode: "environment" },
|
|
config,
|
|
(decodedText) => {
|
|
console.log(`Barcode detected: ${decodedText}`);
|
|
triggerFlash();
|
|
stopScanner().then(() => {
|
|
analyzeProduct({ barcode: decodedText });
|
|
});
|
|
}
|
|
).catch(err => {
|
|
console.error("Scanner start error:", err);
|
|
alert("Could not start camera. Please check permissions.");
|
|
reader.style.display = 'none';
|
|
cleanupUI();
|
|
});
|
|
});
|
|
});
|
|
|
|
btnTakePhoto.addEventListener('click', function() {
|
|
if (!checkHttps()) return;
|
|
|
|
stopScanner().then(() => {
|
|
reader.style.display = 'block';
|
|
reader.classList.add('camera-mode');
|
|
if (!html5QrCode) {
|
|
html5QrCode = new Html5Qrcode("reader");
|
|
}
|
|
|
|
addStopButton();
|
|
|
|
html5QrCode.start(
|
|
{ facingMode: "environment" },
|
|
{ fps: 10 },
|
|
() => { /* ignore QR scans in photo mode unless we want both */ }
|
|
).then(() => {
|
|
if (!document.getElementById('btnCaptureFrame')) {
|
|
const capBtn = document.createElement('button');
|
|
capBtn.id = 'btnCaptureFrame';
|
|
capBtn.innerHTML = '<i class="bi bi-camera-fill me-1"></i> Take Photo & Identify';
|
|
capBtn.className = "btn btn-primary btn-lg w-100 mt-2 mb-1 py-3";
|
|
capBtn.onclick = capturePhoto;
|
|
// Insert before stop button if exists
|
|
const stopBtn = document.getElementById('btnStopScanner');
|
|
if (stopBtn) {
|
|
stopBtn.before(capBtn);
|
|
} else {
|
|
reader.after(capBtn);
|
|
}
|
|
}
|
|
}).catch(err => {
|
|
console.error("Camera start error:", err);
|
|
alert("Camera error: " + err);
|
|
reader.style.display = 'none';
|
|
cleanupUI();
|
|
});
|
|
});
|
|
});
|
|
|
|
function capturePhoto() {
|
|
console.log("Capturing photo...");
|
|
const video = document.querySelector('#reader video');
|
|
if (!video) {
|
|
console.error("Video element not found in reader");
|
|
alert("Camera not ready. Video element missing.");
|
|
return;
|
|
}
|
|
|
|
triggerFlash();
|
|
|
|
const canvas = document.getElementById('photoCanvas');
|
|
if (!canvas) {
|
|
console.error("Canvas element #photoCanvas not found");
|
|
alert("Application error: missing canvas.");
|
|
return;
|
|
}
|
|
|
|
const MAX_DIM = 1024;
|
|
let width = video.videoWidth;
|
|
let height = video.videoHeight;
|
|
|
|
if (width === 0 || height === 0) {
|
|
console.error("Video dimensions are 0", {width, height});
|
|
alert("Camera error: invalid video dimensions. Try moving the camera.");
|
|
return;
|
|
}
|
|
|
|
if (width > height) {
|
|
if (width > MAX_DIM) {
|
|
height *= MAX_DIM / width;
|
|
width = MAX_DIM;
|
|
}
|
|
} else {
|
|
if (height > MAX_DIM) {
|
|
width *= MAX_DIM / height;
|
|
height = MAX_DIM;
|
|
}
|
|
}
|
|
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
const context = canvas.getContext('2d');
|
|
context.drawImage(video, 0, 0, width, height);
|
|
|
|
const dataUrl = canvas.toDataURL('image/jpeg', 0.8);
|
|
console.log("Data URL generated, length:", dataUrl.length);
|
|
|
|
if (dataUrl.length < 1000) {
|
|
console.error("Data URL seems too short, capture might have failed");
|
|
}
|
|
|
|
setTimeout(() => {
|
|
console.log("Proceeding to identification...");
|
|
stopScanner().then(() => {
|
|
analyzeProduct({ image: dataUrl });
|
|
});
|
|
}, 300);
|
|
}
|
|
|
|
function analyzeProduct(params) {
|
|
console.log("Sending request to api/analyze.php", params.barcode ? "with barcode" : "with image");
|
|
aiLoading.style.display = 'block';
|
|
pName.placeholder = "Identifying...";
|
|
|
|
// Clear previous values to show something is happening
|
|
pName.value = '';
|
|
|
|
fetch('api/analyze.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(params)
|
|
})
|
|
.then(res => {
|
|
console.log("Received response status:", res.status);
|
|
return res.json();
|
|
})
|
|
.then(res => {
|
|
console.log("Received data:", res);
|
|
aiLoading.style.display = 'none';
|
|
pName.placeholder = "e.g. Milk, Eggs, Bread";
|
|
|
|
if (res.success && res.data) {
|
|
const data = res.data;
|
|
if (data.name) pName.value = data.name;
|
|
if (data.category) pCategory.value = data.category;
|
|
if (data.quantity) pQuantity.value = data.quantity;
|
|
if (data.expiration_date) pExpiration.value = data.expiration_date;
|
|
|
|
// Visual highlight of changed fields
|
|
[pName, pCategory, pQuantity, pExpiration].forEach(el => {
|
|
if (el.value) {
|
|
el.classList.add('is-valid');
|
|
setTimeout(() => el.classList.remove('is-valid'), 2000);
|
|
}
|
|
});
|
|
|
|
} else {
|
|
console.error("Identification failed:", res.error);
|
|
alert("Could not identify the product: " + (res.error || "Please try again."));
|
|
}
|
|
})
|
|
.catch(err => {
|
|
aiLoading.style.display = 'none';
|
|
pName.placeholder = "e.g. Milk, Eggs, Bread";
|
|
console.error("Fetch error:", err);
|
|
alert("Analysis failed. Connection error or server issue.");
|
|
});
|
|
}
|
|
|
|
const addItemModal = document.getElementById('addItemModal');
|
|
if (addItemModal) {
|
|
addItemModal.addEventListener('hidden.bs.modal', function () {
|
|
stopScanner();
|
|
});
|
|
}
|
|
}); |