396 lines
17 KiB
JavaScript
396 lines
17 KiB
JavaScript
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 = '<option>No cameras found</option>';
|
|
}
|
|
|
|
// 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 = '<option>' + error + '</option>';
|
|
}
|
|
}
|
|
|
|
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 = `
|
|
<p><strong>Title:</strong> ${book.title}</p>
|
|
<p><strong>Author(s):</strong> ${book.authors.join(', ')}</p>
|
|
<p><strong>Published:</strong> ${book.publishDate || 'N/A'}</p>
|
|
<p><strong>ISBN:</strong> ${book.isbn}</p>
|
|
<p><strong>Publisher:</strong> ${book.publishers || 'N/A'}</p>
|
|
<button onclick="selectBook(${index})">Select this book</button>
|
|
`;
|
|
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.
|
|
});
|
|
}
|
|
};
|