let selectedDeviceId; let isScanning = false; // Flag to prevent multiple scans at the same time let quaggaInitialized = false; // Flag to check if Quagga has been initialized // Get available video input devices (cameras) async function getCameras() { try { const cameraSelect = document.getElementById('camera-select'); const stream = await navigator.mediaDevices.getUserMedia({ video: true }); // Prompt for camera access const devices = await navigator.mediaDevices.enumerateDevices(); const videoDevices = devices.filter(device => device.kind === 'videoinput'); if (videoDevices.length > 0) { // Clear any existing options cameraSelect.innerHTML = ''; videoDevices.forEach((device, index) => { const option = document.createElement('option'); option.value = device.deviceId; option.text = device.label || `Camera ${index + 1}`; cameraSelect.appendChild(option); }); // Set selectedDeviceId to the first camera by default selectedDeviceId = cameraSelect.value; startScanner(); // Start the scanner with the default camera } else { console.log("No video devices found."); cameraSelect.innerHTML = ''; } // Update selectedDeviceId when user selects a camera cameraSelect.addEventListener('change', function() { selectedDeviceId = this.value; // Stop Quagga and re-initialize with the new deviceId if (quaggaInitialized) { Quagga.stop(); quaggaInitialized = false; } startScanner(); }); } catch (error) { console.error("Error accessing the camera or enumerating devices:", error); document.getElementById('camera-select').innerHTML = ''; } } function startScanner() { if (!selectedDeviceId) { alert('No camera selected or available.'); return; } const initLogic = () => { console.log("startScanner/initLogic: Proceeding with Quagga.init()."); Quagga.init({ inputStream: { name: "Live", type: "LiveStream", target: document.querySelector('#interactive'), constraints: { deviceId: selectedDeviceId, facingMode: "environment", advanced: [{torch: document.getElementById('flash-toggle') ? document.getElementById('flash-toggle').checked : false}] }, }, decoder: { readers: ["ean_reader", "ean_8_reader"] } }, function(err) { if (err) { console.error("Quagga.init() callback: Failed with error:", err); quaggaInitialized = false; alert('Error initializing scanner: ' + err.name + ' - ' + err.message); return; } console.log("Quagga.init() callback: Successful. Starting stream..."); Quagga.start(); console.log("Quagga.init() callback: Quagga.start() called."); quaggaInitialized = true; Quagga.offDetected(processBarcode); // Remove before adding to prevent duplicates Quagga.onDetected(processBarcode); console.log("Cascade: startScanner/initLogic - Quagga.onDetected(processBarcode) RE-ATTACHED."); console.log("Quagga.init() callback: Stream started. onDetected handler active."); // Explicitly re-apply flash if toggle is on, after stream has started const flashToggleEl = document.getElementById('flash-toggle'); if (flashToggleEl && flashToggleEl.checked) { console.log("Cascade: startScanner/initLogic - Flash toggle is checked. Attempting to re-apply torch constraint post-start."); if (Quagga.CameraAccess && Quagga.CameraAccess.getActiveTrack()) { const track = Quagga.CameraAccess.getActiveTrack(); track.applyConstraints({ advanced: [{ torch: true }] }) .then(() => { console.log("Cascade: startScanner/initLogic - Torch constraint explicitly re-applied successfully post-start."); }) .catch(constraintErr => { console.error("Cascade: startScanner/initLogic - Error explicitly re-applying torch constraint post-start:", constraintErr); }); } else { console.warn("Cascade: startScanner/initLogic - Flash toggle checked, but cannot get active track to re-apply torch constraint post-start."); } } }); }; if (quaggaInitialized) { console.log("startScanner: Restart requested. Stopping current Quagga instance."); try { Quagga.stop(); console.log("startScanner: Quagga.stop() called successfully during restart."); } catch (e) { console.warn("startScanner: Error during Quagga.stop(), proceeding with re-init attempt:", e); } quaggaInitialized = false; // Force re-init console.log("startScanner: Delaying for 100ms before re-initializing Quagga for restart."); setTimeout(() => { console.log("startScanner: Delay finished. Calling initLogic for restart."); initLogic(); }, 100); // 100ms delay } else { console.log("startScanner: Initial Quagga setup or forced re-init without prior true state."); initLogic(); } } async function processBarcode(data) { console.log("Cascade: processBarcode - Entered. Current isScanning state:", isScanning); if (isScanning) { console.log("Cascade: processBarcode - Exiting early because isScanning is true."); return; // Prevent further scans while a scan is being processed } isScanning = true; // Set the scanning flag console.log("Cascade: processBarcode - Set isScanning to true."); const isbn = data.codeResult.code; console.log("Detected code:", isbn); // Validate ISBN length (10 or 13 digits) if (!isbn || (isbn.length !== 10 && isbn.length !== 13)) { console.log(`Cascade: processBarcode - Invalid code length: ${isbn.length}. Code: ${isbn}. Resuming scan.`); // isScanning = false; // Reset isScanning to allow immediate re-scan by Quagga's internal loop if needed // No need to explicitly set isScanning to false here if we don't stop Quagga. // The initial check `if (isScanning)` at the top of processBarcode should handle concurrent calls. // We want Quagga to continue, so we don't call Quagga.stop() or startScanner() here. return; // Continue scanning } console.log("Validated ISBN:", isbn); Quagga.stop(); // Stop the scanner once a VALID ISBN is detected if (document.getElementById('confirm-mode').checked) { // Confirm book in library await confirmBookInLibrary(isbn); } else { // Normal flow await fetchBookInfo(isbn); } isScanning = false; // Reset the scanning flag once processing is done } async function fetchBookInfo(isbn) { try { const response = await fetch(`/book/${isbn}`); const bookData = await response.json(); if (bookData.title) { bookData.isbn2 = isbn; // Add the ISBN to the book data promptUserWithBook(bookData); } else { console.log("Cascade: fetchBookInfo - No book data. Resetting isScanning."); isScanning = false; // Reset flag BEFORE restarting scanner console.log("Cascade: fetchBookInfo - isScanning is now false. Calling startScanner..."); startScanner(); // Restart the scanner if no book information is found } } catch (error) { console.error('Error fetching book data:', error); console.log("Cascade: fetchBookInfo - Error fetching. Resetting isScanning."); isScanning = false; // Reset flag BEFORE restarting scanner console.log("Cascade: fetchBookInfo - isScanning is now false (error path). Calling startScanner..."); startScanner(); // Restart the scanner on error as well } } function promptUserWithBook(bookData) { // Prepare the information to display in the confirm dialog const title = bookData.data.title; const authors = bookData.data.authors ? (Array.isArray(bookData.data.authors) ? bookData.data.authors.join(', ') : bookData.data.authors) : 'Unknown Author'; const description = bookData.data.description || 'No description available'; // Combine the information into a single message const message = `Title: ${title}\nAuthor(s): ${authors}\nDescription: ${description}\n\nDo you want to add this book to the database?`; // Use the built-in confirm prompt to ask the user const userConfirmed = confirm(message); if (userConfirmed) { console.log('User confirmed to add the book to the database.'); storeBookInDatabase(bookData); } else { console.log('User declined to add the book to the database.'); startScanner(); // Restart the scanner if the user declines } } async function storeBookInDatabase(bookData) { try { const response = await fetch('/store-book', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ isbn: bookData.isbn || bookData.isbn2, title: bookData.title, authors: bookData.authors, publishedDate: bookData.publishedDate, description: bookData.description, url: bookData.url, number_of_pages: bookData.number_of_pages, identifiers: bookData.identifiers, publishers: bookData.publishers, subjects: bookData.subjects, notes: bookData.notes, cover_small: bookData.cover_small, cover_medium: bookData.cover_medium, cover_large: bookData.cover_large }) }); const result = await response.json(); if (result.success) { alert('Book added to the database successfully!'); } else { alert('Failed to add the book to the database.'); } } catch (error) { console.error('Error storing book in database:', error); alert('An error occurred while storing the book.'); } finally { startScanner(); // Restart the scanner after storing the book or handling errors } } async function confirmBookInLibrary(isbn) { try { const response = await fetch(`/book/confirm/${isbn}`); const bookData = await response.json(); console.log("Cascade: confirmBookInLibrary - Response from /book/confirm:", bookData); if (bookData.data && bookData.data.title) { // Display the book information and a success message const title = bookData.data.title; const authors = bookData.data.authors ? (Array.isArray(bookData.data.authors) ? bookData.data.authors.join(', ') : bookData.data.authors) : 'Unknown Author'; const description = bookData.data.description || 'No description available'; const message = `Title: ${title}\nAuthor(s): ${authors}\nDescription: ${description}\n\nBook found in the library!`; alert(message); } else { alert('Book not found in the library.'); } } catch (error) { console.error('Error confirming book in library:', error); alert('An error occurred while confirming the book in the library.'); } finally { console.log("Cascade: confirmBookInLibrary - finally block. Resetting isScanning."); isScanning = false; // Reset flag BEFORE restarting scanner console.log("Cascade: confirmBookInLibrary - isScanning is now false. Calling startScanner..."); startScanner(); // Restart the scanner after processing } } // Add an event listener for the search button document.getElementById('search-title').addEventListener('click', function() { const title = document.getElementById('title-input').value; searchByTitle(title); }); async function searchByTitle(title = '') { if (!title) { alert('Please enter a book title to search.'); return; } try { const response = await fetch(`/search-title?title=${encodeURIComponent(title)}`); const data = await response.json(); if (data.results && data.results.length > 0) { displaySearchResults(data.results); } else { alert('No books found with that title.'); } } catch (error) { console.error('Error searching for book by title:', error); alert('An error occurred while searching for the book.'); } } function displaySearchResults(results) { // Display the search results and allow the user to select one const bookInfoDiv = document.getElementById('book-info'); bookInfoDiv.innerHTML = ''; // Clear previous results results.forEach((book, index) => { const bookElement = document.createElement('div'); bookElement.innerHTML = `
Title: ${book.title}
Author(s): ${book.authors.join(', ')}
Published: ${book.publishDate || 'N/A'}
ISBN: ${book.isbn}
Publisher: ${book.publishers || 'N/A'}
`; bookInfoDiv.appendChild(bookElement); }); // Store the results so that we can reference them when the user makes a selection window.searchResults = results; } // Client-side function to request checkout function requestCheckout(isbn) { fetch(`/api/checkout/${isbn}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Basic ' + btoa(username + ':' + password) } }) .then(response => response.json()) .then(data => { if (data.success) { alert('Checkout request sent successfully.'); } else { alert('Failed to send checkout request.'); } }) .catch(error => { console.error('Error:', error); alert('An error occurred while sending the checkout request.'); }); } function selectBook(index) { const selectedBook = window.searchResults[index]; console.log('Selected book:', selectedBook); // You can now fetch more details using the unique key if needed, or directly store this in your database const isbn = selectedBook.isbn; if (isbn) { fetchBookInfo(isbn); } else { promptUserWithBook(selectedBook); // You might have to adapt this if there's no ISBN } } // Get cameras on page load window.onload = function() { getCameras(); searchByTitle(''); const flashToggle = document.getElementById('flash-toggle'); if (flashToggle) { flashToggle.addEventListener('change', function() { if (quaggaInitialized && Quagga.CameraAccess && Quagga.CameraAccess.getActiveTrack()) { const track = Quagga.CameraAccess.getActiveTrack(); const constraints = { advanced: [{ torch: this.checked }] }; track.applyConstraints(constraints) .then(() => { console.log("Flash toggled via applyConstraints:", this.checked); }) .catch(err => { console.error("Error applying flash constraint dynamically:", err, ". Falling back to scanner restart."); // Fallback: restart scanner if (quaggaInitialized) { Quagga.stop(); quaggaInitialized = false; startScanner(); // This will pick up the new toggle state from the checkbox } }); } else if (quaggaInitialized) { // If getActiveTrack is not available or Quagga is initialized but track isn't, restart scanner. console.log("Flash toggled, getActiveTrack not available or other issue, restarting scanner for change to take effect."); Quagga.stop(); quaggaInitialized = false; // Force re-init startScanner(); // This will pick up the new toggle state } // If Quagga is not yet initialized, startScanner() will pick up the state when it's called. }); } };