improve photos

This commit is contained in:
O K
2025-11-25 11:00:26 +02:00
parent ff2dcdc0ee
commit 8e7141175a
6 changed files with 1004 additions and 719 deletions

View File

@@ -1,309 +1,254 @@
document.addEventListener('DOMContentLoaded', () => {
// --- DOM Element References ---
const videoContainer = document.getElementById('alp-video-container');
// 1. Elements
const video = document.getElementById('alp-video');
const canvas = document.getElementById('alp-canvas');
const overlay = document.getElementById('alp-viewfinder-overlay');
const overlayText = document.getElementById('alp-overlay-text');
const cameraSelector = document.getElementById('alp-camera-selector');
const manualInputForm = document.getElementById('alp-manual-form');
const productInfoSection = document.getElementById('alp-product-info');
const overlayText = document.getElementById('alp-status-text');
const stepScan = document.getElementById('alp-step-scan');
const stepAction = document.getElementById('alp-step-action');
// Product Data Elements
const productNameEl = document.getElementById('alp-product-name');
const productPricesEl = document.getElementById('alp-product-prices');
const existingPhotosSection = document.getElementById('alp-existing-photos');
const existingPhotosContainer = document.getElementById('alp-photos-container');
const messageArea = document.getElementById('alp-message-area');
const photoListEl = document.getElementById('alp-photo-list');
// Buttons
const manualForm = document.getElementById('alp-manual-form');
const btnExpiry = document.getElementById('btn-snap-expiry');
const btnPkg = document.getElementById('btn-snap-packaging');
const btnReset = document.getElementById('btn-reset');
// --- State Management ---
const AppState = {
IDLE: 'idle', // Camera off, welcome message
READY_TO_SCAN: 'ready_to_scan', // Camera on, waiting for tap to scan
SCANNING: 'scanning', // Actively looking for barcode
PRODUCT_FOUND: 'product_found', // Product found, waiting for tap to take photo
UPLOADING: 'uploading' // Photo is being sent to server
};
let currentState = AppState.IDLE;
// State
let currentStream = null;
let barcodeDetector = null;
let isScanning = false;
let currentProductId = null;
const ajaxUrl = window.addLivePhotoAjaxUrl || '';
// --- Initialization ---
if (!('BarcodeDetector' in window)) {
showMessage('Barcode Detector API is not supported. Please use manual input.', true);
} else {
barcodeDetector = new BarcodeDetector({ formats: ['ean_13'] });
}
if (!navigator.mediaDevices) {
showMessage('Camera access is not supported in this browser.', true);
} else {
populateCameraSelector();
}
// 2. Initialize
initBarcodeDetector();
startCamera();
// 3. Event Listeners
manualForm.addEventListener('submit', (e) => {
e.preventDefault();
const val = document.getElementById('alp-manual-input').value.trim();
if(val) fetchProduct(val);
});
btnReset.addEventListener('click', resetApp);
updateUIForState(AppState.IDLE); // Set initial UI state
btnExpiry.addEventListener('click', () => takePhoto('expiry'));
btnPkg.addEventListener('click', () => takePhoto('packaging'));
// --- Event Listeners ---
videoContainer.addEventListener('click', handleViewfinderTap);
cameraSelector.addEventListener('change', handleCameraChange);
manualInputForm.addEventListener('submit', handleManualSubmit);
existingPhotosContainer.addEventListener('click', handleDeleteClick);
// --- Core Functions ---
// --- Core Logic ---
function handleViewfinderTap() {
switch (currentState) {
case AppState.IDLE:
startCamera();
break;
case AppState.READY_TO_SCAN:
detectBarcode();
break;
case AppState.PRODUCT_FOUND:
takePhoto();
break;
async function initBarcodeDetector() {
if ('BarcodeDetector' in window) {
// Check supported formats
const formats = await BarcodeDetector.getSupportedFormats();
if (formats.includes('ean_13')) {
barcodeDetector = new BarcodeDetector({ formats: ['ean_13'] });
console.log('BarcodeDetector ready');
} else {
overlayText.textContent = "EAN13 not supported by device";
}
} else {
console.warn('BarcodeDetector API not supported in this browser');
overlayText.textContent = "Auto-scan not supported. Use manual input.";
}
}
function updateUIForState(newState, customText = null) {
currentState = newState;
let textContent = '';
overlay.style.display = 'flex';
switch (newState) {
case AppState.IDLE:
textContent = "Tap to Start Camera";
break;
case AppState.READY_TO_SCAN:
textContent = "Tap to Scan Barcode";
break;
case AppState.SCANNING:
textContent = `<div class="spinner"></div>`;
break;
case AppState.PRODUCT_FOUND:
textContent = "Tap to Take Picture";
break;
case AppState.UPLOADING:
textContent = "Uploading...";
break;
}
overlayText.innerHTML = customText || textContent;
}
async function startCamera() {
if (currentStream) return;
const constraints = { video: { deviceId: cameraSelector.value ? { exact: cameraSelector.value } : undefined, facingMode: 'environment' } };
try {
const constraints = {
video: {
facingMode: 'environment', // Rear camera
width: { ideal: 1280 },
height: { ideal: 720 }
}
};
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
video.srcObject = currentStream;
await video.play();
updateUIForState(AppState.READY_TO_SCAN);
// Wait for video to be ready
video.onloadedmetadata = () => {
video.play();
if(barcodeDetector) {
isScanning = true;
overlayText.textContent = "Scan Barcode...";
scanLoop();
} else {
overlayText.textContent = "Camera Ready (Manual Mode)";
}
};
} catch (err) {
console.error('Error accessing camera:', err);
stopCamera(); // Ensure everything is reset
updateUIForState(AppState.IDLE, 'Camera Error. Tap to retry.');
console.error(err);
overlayText.textContent = "Camera Access Denied or Error";
}
}
function stopCamera() {
if (currentStream) {
currentStream.getTracks().forEach(track => track.stop());
currentStream = null;
}
video.srcObject = null;
updateUIForState(AppState.IDLE);
}
async function scanLoop() {
if (!isScanning || !barcodeDetector || currentProductId) return;
async function detectBarcode() {
if (!barcodeDetector || currentState !== AppState.READY_TO_SCAN) return;
updateUIForState(AppState.SCANNING);
try {
const barcodes = await barcodeDetector.detect(video);
if (barcodes.length > 0) {
searchProduct(barcodes[0].rawValue);
} else {
showMessage('No barcode found. Please try again.', true);
updateUIForState(AppState.READY_TO_SCAN);
const code = barcodes[0].rawValue;
isScanning = false; // Stop scanning
fetchProduct(code);
return;
}
} catch (err) {
console.error('Barcode detection error:', err);
showMessage('Error during barcode detection.', true);
updateUIForState(AppState.READY_TO_SCAN);
} catch (e) {
// Detection error (common in loop)
}
}
function takePhoto() {
if (!currentStream || !currentProductId || currentState !== AppState.PRODUCT_FOUND) return;
updateUIForState(AppState.UPLOADING);
const targetWidth = 800, targetHeight = 800;
canvas.width = targetWidth; canvas.height = targetHeight;
const ctx = canvas.getContext('2d');
const videoWidth = video.videoWidth, videoHeight = video.videoHeight;
const size = Math.min(videoWidth, videoHeight);
const x = (videoWidth - size) / 2, y = (videoHeight - size) / 2;
ctx.drawImage(video, x, y, size, size, 0, 0, targetWidth, targetHeight);
const imageData = canvas.toDataURL('image/webp', 0.8);
uploadImage(imageData);
}
function resetForNextProduct() {
currentProductId = null;
productInfoSection.style.display = 'none';
existingPhotosSection.style.display = 'none';
existingPhotosContainer.innerHTML = '';
updateUIForState(AppState.READY_TO_SCAN);
// Scan every 200ms to save battery
setTimeout(() => requestAnimationFrame(scanLoop), 200);
}
// --- AJAX and Helper Functions ---
async function searchProduct(identifier) {
const formData = new FormData();
formData.append('action', 'searchProduct'); formData.append('identifier', identifier);
try {
const response = await fetch(ajaxUrl, { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
const product = result.data;
currentProductId = product.id_product;
displayProductInfo(product);
updateUIForState(AppState.PRODUCT_FOUND);
} else {
showMessage(result.message, true);
updateUIForState(AppState.READY_TO_SCAN);
}
} catch (err) {
showMessage('Network error searching for product.', true);
updateUIForState(AppState.READY_TO_SCAN);
}
}
async function uploadImage(imageData) {
const formData = new FormData();
formData.append('action', 'uploadImage'); formData.append('id_product', currentProductId); formData.append('imageData', imageData);
try {
const response = await fetch(ajaxUrl, { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
showMessage(result.message, false);
appendNewPhoto(result.data.new_photo);
setTimeout(resetForNextProduct, 1500); // Pause to show success, then reset
} else {
showMessage(result.message, true);
updateUIForState(AppState.PRODUCT_FOUND); // Allow user to try photo again
}
} catch (err) {
showMessage('Network error uploading photo.', true);
updateUIForState(AppState.PRODUCT_FOUND);
}
}
function fetchProduct(identifier) {
overlayText.textContent = "Searching...";
isScanning = false;
async function populateCameraSelector() { /* (This function can remain from previous versions) */ }
function handleCameraChange() { /* (This function can remain from previous versions) */ }
function handleManualSubmit(e) { /* (This function can remain from previous versions) */ }
function handleDeleteClick(e) { /* (This function can remain from previous versions) */ }
function displayProductInfo(product) { /* (This function can remain from previous versions) */ }
function appendNewPhoto(photo) { /* (This function can remain from previous versions) */ }
function showMessage(text, isError = false) { /* (This function can remain from previous versions) */ }
const fd = new FormData();
fd.append('action', 'searchProduct');
fd.append('identifier', identifier);
// --- Re-pasting the helper functions for completeness ---
async function populateCameraSelector() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(device => device.kind === 'videoinput');
cameraSelector.innerHTML = '';
videoDevices.forEach((device, index) => {
const option = document.createElement('option');
option.value = device.deviceId;
option.textContent = device.label || `Camera ${index + 1}`;
cameraSelector.appendChild(option);
fetch(window.alpAjaxUrl, { method: 'POST', body: fd })
.then(res => res.json())
.then(data => {
if (data.success) {
loadProductView(data.data);
} else {
alert(data.message || 'Product not found');
resetApp(); // Go back to scanning
}
})
.catch(err => {
console.error(err);
alert('Network Error');
resetApp();
});
const preferredCameraId = localStorage.getItem('addLivePhoto_preferredCameraId');
if (preferredCameraId && cameraSelector.querySelector(`option[value="${preferredCameraId}"]`)) {
cameraSelector.value = preferredCameraId;
}
} catch (err) { console.error('Error enumerating devices:', err); }
}
function handleCameraChange() {
localStorage.setItem('addLivePhoto_preferredCameraId', cameraSelector.value);
if (currentStream) { // If camera is active, restart it with the new selection
stopCamera();
startCamera();
}
}
function handleManualSubmit(e) {
e.preventDefault();
const identifier = document.getElementById('alp-manual-identifier').value.trim();
if (identifier) {
showMessage(`Searching for: ${identifier}...`);
searchProduct(identifier);
}
function loadProductView(productData) {
currentProductId = productData.id_product;
productNameEl.textContent = `[${productData.reference}] ${productData.name}`;
renderPhotos(productData.existing_photos);
// Switch View
stepScan.style.display = 'none';
stepAction.style.display = 'block';
}
function handleDeleteClick(e) {
if (e.target && e.target.classList.contains('delete-photo-btn')) {
const button = e.target;
const imageName = button.dataset.imageName;
const productId = button.dataset.productId;
if (confirm(`Are you sure you want to delete this photo?`)) {
// Simplified delete without a dedicated function
const formData = new FormData();
formData.append('action', 'deleteImage');
formData.append('id_product', productId);
formData.append('image_name', imageName);
fetch(ajaxUrl, { method: 'POST', body: formData })
.then(res => res.json())
.then(result => {
if (result.success) {
showMessage(result.message, false);
button.closest('.photo-thumb').remove();
} else {
showMessage(result.message, true);
}
}).catch(err => showMessage('Network error deleting photo.', true));
}
}
function takePhoto(type) {
if (!currentProductId) return;
// Capture frame
const w = video.videoWidth;
const h = video.videoHeight;
// Crop to square (center)
const size = Math.min(w, h);
const x = (w - size) / 2;
const y = (h - size) / 2;
canvas.width = 800;
canvas.height = 800;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, x, y, size, size, 0, 0, 800, 800);
const dataUrl = canvas.toDataURL('image/webp', 0.8);
// Upload
const fd = new FormData();
fd.append('action', 'uploadImage');
fd.append('id_product', currentProductId);
fd.append('image_type', type);
fd.append('imageData', dataUrl);
// Visual feedback
const btn = (type === 'expiry') ? btnExpiry : btnPkg;
const originalText = btn.innerHTML;
btn.innerHTML = 'Uploading...';
btn.disabled = true;
fetch(window.alpAjaxUrl, { method: 'POST', body: fd })
.then(res => res.json())
.then(data => {
if (data.success) {
// Add new photo to list without reload
addPhotoToDom(data.photo);
// Flash success message
alert(`Saved as ${type}!`);
} else {
alert('Error: ' + data.message);
}
})
.catch(err => alert('Upload failed'))
.finally(() => {
btn.innerHTML = originalText;
btn.disabled = false;
});
}
function displayProductInfo(product) {
productNameEl.textContent = `[ID: ${product.id_product}] ${product.name}`;
let pricesHtml = `Wholesale: ${product.wholesale_price} | Sale: ${product.retail_price}`;
if (product.discounted_price) {
pricesHtml += ` | <strong class="text-danger">Discounted: ${product.discounted_price}</strong>`;
}
productPricesEl.innerHTML = pricesHtml;
renderExistingPhotos(product.existing_photos, product.id_product);
productInfoSection.style.display = 'block';
}
function renderExistingPhotos(photos, productId) {
existingPhotosContainer.innerHTML = '';
if (photos && photos.length > 0) {
existingPhotosSection.style.display = 'block';
photos.forEach(photo => appendNewPhoto(photo, productId));
function renderPhotos(photos) {
photoListEl.innerHTML = '';
if(photos && photos.length) {
photos.forEach(addPhotoToDom);
} else {
existingPhotosSection.style.display = 'none';
photoListEl.innerHTML = '<p class="text-muted">No photos yet.</p>';
}
}
function appendNewPhoto(photo, productId = currentProductId) {
const thumbDiv = document.createElement('div');
thumbDiv.className = 'photo-thumb';
thumbDiv.innerHTML = `
<a href="${photo.url}" target="_blank">
<img src="${photo.url}" alt="Live photo" loading="lazy" />
</a>
<button class="btn btn-sm btn-danger delete-photo-btn" data-product-id="${productId}" data-image-name="${photo.name}">X</button>
function addPhotoToDom(photo) {
// Remove "No photos" msg if exists
if (photoListEl.querySelector('p')) photoListEl.innerHTML = '';
const div = document.createElement('div');
div.className = 'alp-thumb';
const badgeClass = (photo.type === 'expiry') ? 'badge-success' : 'badge-info';
div.innerHTML = `
<img src="${photo.url}" target="_blank">
<span class="badge ${badgeClass}">${photo.type}</span>
<button class="btn-delete" onclick="deletePhoto(${currentProductId}, '${photo.name}', this)">×</button>
`;
existingPhotosContainer.prepend(thumbDiv);
existingPhotosSection.style.display = 'block';
photoListEl.prepend(div);
}
function showMessage(text, isError = false) {
messageArea.textContent = text;
messageArea.className = isError ? 'alert alert-danger' : 'alert alert-info';
messageArea.style.display = 'block';
setTimeout(() => { messageArea.style.display = 'none'; }, 4000); // Message disappears after 4s
function resetApp() {
currentProductId = null;
document.getElementById('alp-manual-input').value = '';
stepAction.style.display = 'none';
stepScan.style.display = 'block';
if(barcodeDetector) {
isScanning = true;
overlayText.textContent = "Scan Barcode...";
scanLoop();
} else {
overlayText.textContent = "Camera Ready (Manual Mode)";
}
}
// Expose delete function globally so onclick in HTML works
window.deletePhoto = function(idProduct, imgName, btnEl) {
if(!confirm('Delete this photo?')) return;
const fd = new FormData();
fd.append('action', 'deleteImage');
fd.append('id_product', idProduct);
fd.append('image_name', imgName);
fetch(window.alpAjaxUrl, { method: 'POST', body: fd })
.then(res => res.json())
.then(data => {
if(data.success) {
btnEl.closest('.alp-thumb').remove();
} else {
alert('Error deleting');
}
});
};
});