feat: add book metadata editor with Google Books and OpenLibrary integration
This commit is contained in:
parent
f4f227b24d
commit
d2b88f4e59
@ -3,7 +3,13 @@ const axios = require('axios');
|
||||
|
||||
// Fetch book from the local database by ISBN
|
||||
const fetchBookFromLocalDatabase = async (isbn) => {
|
||||
return await Book.findOne({ where: { isbn } });
|
||||
return await Book.findOne({
|
||||
where: { isbn },
|
||||
include: [{
|
||||
model: Location,
|
||||
as: 'Location'
|
||||
}]
|
||||
});
|
||||
};
|
||||
|
||||
// Fetch book from Google Books API
|
||||
|
||||
183
index.js
183
index.js
@ -130,6 +130,16 @@ app.get('/api/books-with-images', async (req, res) => {
|
||||
},
|
||||
attributes: ['isbn', 'title', 'cover_small', 'cover_medium', 'cover_large', 'authors'],
|
||||
});
|
||||
// CORRECTED DEBUG LOGGING ON SERVER
|
||||
if (books && books.length > 0) {
|
||||
// Log a specific book if its ID is known, e.g., ID 207 for Artemis Fowl
|
||||
const specificBookForDebug = books.find(b => b.id === 207);
|
||||
if (specificBookForDebug) {
|
||||
console.log("Server-side specific book from /api/books (ID 207):", JSON.stringify(specificBookForDebug, null, 2));
|
||||
} else {
|
||||
console.log("Server-side book ID 207 not found in /api/books results, logging first book instead:", JSON.stringify(books[0], null, 2));
|
||||
}
|
||||
}
|
||||
res.json(books);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch books with images:', error);
|
||||
@ -301,23 +311,25 @@ function delay(ms) {
|
||||
// Confirm book is in the library by ISBN
|
||||
app.get('/book/confirm/:isbn', async (req, res) => {
|
||||
const { isbn } = req.params;
|
||||
console.log(`Fetching book data for ISBN: ${isbn}`);
|
||||
console.log(`Cascade: /book/confirm/:isbn - Confirming ISBN: ${isbn}`);
|
||||
|
||||
try {
|
||||
// Check if the book is in the local database
|
||||
const localBook = await fetchBookFromLocalDatabase(isbn);
|
||||
|
||||
if (localBook) {
|
||||
console.log('Book found in the local database');
|
||||
console.log('Cascade: /book/confirm/:isbn - Book found in local database.');
|
||||
return res.json({ source: 'local', data: localBook });
|
||||
} else {
|
||||
console.log('Cascade: /book/confirm/:isbn - Book NOT found in local database.');
|
||||
return res.status(404).json({ error: 'Book not found in local library' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({ error: 'Failed to fetch book data' });
|
||||
console.error('Cascade: /book/confirm/:isbn - Error during lookup:', error);
|
||||
return res.status(500).json({ error: 'Failed to confirm book in local library' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.get('/search-title', async (req, res) => {
|
||||
const { title, internalOnly = false } = req.query;
|
||||
console.log(`Searching for books by title or related fields: ${title}`);
|
||||
@ -393,9 +405,14 @@ app.delete('/book/:id', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/locations', async (req, res) => {
|
||||
app.get('/api/locations', async (req, res) => {
|
||||
try {
|
||||
const locations = await Location.findAll();
|
||||
const locations = await Location.findAll({
|
||||
order: [
|
||||
['name', 'ASC'],
|
||||
['shelf', 'ASC']
|
||||
]
|
||||
});
|
||||
res.json(locations);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch locations:', error);
|
||||
@ -657,6 +674,156 @@ function requestCheckout(isbn) {
|
||||
});
|
||||
}
|
||||
|
||||
// API endpoint to get all books
|
||||
app.get('/api/books', async (req, res) => {
|
||||
try {
|
||||
const books = await Book.findAll({
|
||||
include: [{
|
||||
model: Location,
|
||||
as: 'Location' // Corrected alias based on model definition
|
||||
}],
|
||||
order: [['title', 'ASC']]
|
||||
});
|
||||
res.json(books.map(book => ({
|
||||
id: book.id,
|
||||
title: book.title,
|
||||
authors: book.authors,
|
||||
isbn: book.isbn,
|
||||
publishedDate: book.publishedDate,
|
||||
description: book.description,
|
||||
cover_small: book.cover_small,
|
||||
number_of_pages: book.number_of_pages,
|
||||
publishers: book.publishers,
|
||||
subjects: book.subjects,
|
||||
location_id: book.location_id,
|
||||
Location: book.Location ? { id: book.Location.id, name: book.Location.name, room: book.Location.room, shelf: book.Location.shelf } : null
|
||||
})));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch books:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch books' });
|
||||
}
|
||||
});
|
||||
|
||||
// API endpoint to add a new book
|
||||
app.post('/api/books', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
title,
|
||||
authors,
|
||||
publishedDate,
|
||||
description,
|
||||
isbn,
|
||||
number_of_pages,
|
||||
publishers,
|
||||
subjects,
|
||||
cover_small,
|
||||
cover_medium,
|
||||
cover_large,
|
||||
location_id
|
||||
} = req.body;
|
||||
|
||||
// Basic validation (e.g., title is required)
|
||||
if (!title) {
|
||||
return res.status(400).json({ error: 'Title is required' });
|
||||
}
|
||||
|
||||
const newBookData = {
|
||||
title,
|
||||
authors: Array.isArray(authors) ? authors.join(', ') : authors,
|
||||
publishedDate,
|
||||
description,
|
||||
isbn,
|
||||
number_of_pages,
|
||||
publishers: Array.isArray(publishers) ? publishers.join(', ') : publishers,
|
||||
subjects: Array.isArray(subjects) ? subjects.join(', ') : subjects,
|
||||
cover_small,
|
||||
cover_medium,
|
||||
cover_large,
|
||||
location_id: location_id === '' ? null : location_id // Handle empty string as null
|
||||
};
|
||||
|
||||
const book = await Book.create(newBookData);
|
||||
|
||||
// Fetch the newly created book with its location to send back in the response
|
||||
const createdBookWithLocation = await Book.findByPk(book.id, {
|
||||
include: [{
|
||||
model: Location,
|
||||
as: 'Location'
|
||||
}]
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, book: createdBookWithLocation });
|
||||
} catch (error) {
|
||||
console.error('Failed to add new book:', error);
|
||||
if (error.name === 'SequelizeUniqueConstraintError') {
|
||||
let errorMessage = 'Failed to add new book due to a conflict.';
|
||||
// Assuming 'isbn' is a unique field in your Book model
|
||||
if (error.fields && typeof error.fields === 'object' && 'isbn' in error.fields) {
|
||||
errorMessage = 'A book with this ISBN already exists. Please use the edit feature if you want to update it.';
|
||||
}
|
||||
return res.status(409).json({ error: errorMessage });
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to add new book' });
|
||||
}
|
||||
});
|
||||
|
||||
// API endpoint to update a book's metadata by its primary key ID
|
||||
app.put('/api/books/:id', async (req, res) => {
|
||||
try {
|
||||
const bookId = req.params.id;
|
||||
const book = await Book.findByPk(bookId);
|
||||
|
||||
if (!book) {
|
||||
return res.status(404).json({ error: 'Book not found' });
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
authors,
|
||||
publishedDate,
|
||||
description,
|
||||
isbn,
|
||||
number_of_pages,
|
||||
publishers,
|
||||
subjects,
|
||||
cover_small,
|
||||
cover_medium,
|
||||
cover_large,
|
||||
location_id // Added to handle location updates
|
||||
} = req.body;
|
||||
|
||||
const updateData = {};
|
||||
if (title !== undefined) updateData.title = title;
|
||||
if (authors !== undefined) updateData.authors = Array.isArray(authors) ? authors.join(', ') : authors;
|
||||
if (publishedDate !== undefined) updateData.publishedDate = publishedDate;
|
||||
if (description !== undefined) updateData.description = description;
|
||||
if (isbn !== undefined) updateData.isbn = isbn === '' ? null : isbn;
|
||||
if (number_of_pages !== undefined) updateData.number_of_pages = number_of_pages;
|
||||
if (publishers !== undefined) updateData.publishers = Array.isArray(publishers) ? publishers.join(', ') : publishers;
|
||||
if (subjects !== undefined) updateData.subjects = Array.isArray(subjects) ? subjects.join(', ') : subjects;
|
||||
if (cover_small !== undefined) updateData.cover_small = cover_small;
|
||||
if (cover_medium !== undefined) updateData.cover_medium = cover_medium;
|
||||
if (cover_large !== undefined) updateData.cover_large = cover_large;
|
||||
if (location_id !== undefined) updateData.location_id = location_id === '' ? null : location_id; // Handle empty string as null
|
||||
|
||||
await book.update(updateData);
|
||||
console.log(`Book data for ID ${bookId} updated successfully.`);
|
||||
|
||||
// Fetch the updated book with its location to send back in the response
|
||||
const updatedBookWithLocation = await Book.findByPk(bookId, {
|
||||
include: [{
|
||||
model: Location,
|
||||
as: 'Location'
|
||||
}]
|
||||
});
|
||||
res.json({ success: true, book: updatedBookWithLocation });
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Failed to update book with ID ${req.params.id}:`, error);
|
||||
res.status(500).json({ error: 'Failed to update book' });
|
||||
}
|
||||
});
|
||||
|
||||
const httpsServer = https.createServer(credentials, app);
|
||||
|
||||
|
||||
@ -698,7 +865,7 @@ app.get('/api/books-on-loan', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/book/update/:isbn', async (req, res) => {
|
||||
app.post('/book/update/:isbn', authMiddleware, async (req, res) => { // Added authMiddleware for consistency
|
||||
const { isbn } = req.params;
|
||||
const googleBooksApiKey = process.env.GOOGLE_BOOKS_API_KEY;
|
||||
|
||||
|
||||
53
populate_locations.js
Normal file
53
populate_locations.js
Normal file
@ -0,0 +1,53 @@
|
||||
|
||||
import { sequelize, Location } from './models.js';
|
||||
|
||||
const locationsToCreate = [];
|
||||
|
||||
// Cubes 1-10
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
locationsToCreate.push({ name: 'Cube', shelf: i.toString() });
|
||||
}
|
||||
|
||||
// Office Shelves 1-6
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
locationsToCreate.push({ name: 'Office Shelf', shelf: i.toString() });
|
||||
}
|
||||
|
||||
// Miscellaneous placements
|
||||
locationsToCreate.push({ name: 'Upstairs', shelf: 'Misc' });
|
||||
locationsToCreate.push({ name: 'Downstairs', shelf: 'Misc' });
|
||||
|
||||
async function populateLocations() {
|
||||
try {
|
||||
// Optional: Sync database to ensure tables are created
|
||||
await sequelize.sync(); // Often done in your main app setup
|
||||
|
||||
// Check if locations already exist to avoid duplicates if script is run multiple times
|
||||
// This is a simple check; more robust checks might be needed for production
|
||||
const existingLocations = await Location.findAll();
|
||||
const newLocations = locationsToCreate.filter(ltc =>
|
||||
!existingLocations.some(el => el.name === ltc.name && el.shelf === ltc.shelf)
|
||||
);
|
||||
|
||||
if (newLocations.length > 0) {
|
||||
await Location.bulkCreate(newLocations);
|
||||
console.log(`${newLocations.length} locations have been successfully added.`);
|
||||
} else {
|
||||
console.log('All specified locations already exist in the database. No new locations added.');
|
||||
}
|
||||
|
||||
// Select all locations from the database
|
||||
const [locationsFromQuery] = await sequelize.query('SELECT * FROM locations'); // sequelize.query returns [results, metadata]
|
||||
console.log('All locations:', locationsFromQuery);
|
||||
} catch (error) {
|
||||
console.error('Error populating locations:', error);
|
||||
} finally {
|
||||
// Close the database connection if the script is standalone and not part of a larger app run
|
||||
if (sequelize && typeof sequelize.close === 'function') {
|
||||
await sequelize.close();
|
||||
console.log('Database connection closed.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
populateLocations();
|
||||
59
public/edit_books.html
Normal file
59
public/edit_books.html
Normal file
@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Edit Book Metadata</title>
|
||||
<link rel="stylesheet" href="style.css"> </head>
|
||||
<body>
|
||||
<h1>Edit Book Metadata</h1>
|
||||
|
||||
<button id="add-new-book-button" style="margin-bottom: 10px;">Add New Book</button>
|
||||
<div id="book-list-container">
|
||||
<!-- Books will be listed here by JavaScript -->
|
||||
</div>
|
||||
|
||||
<!-- Modal for editing metadata -->
|
||||
<div id="edit-modal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<span class="close-button">×</span>
|
||||
<h2>Edit Metadata for <span id="modal-book-title"></span></h2>
|
||||
<input type="hidden" id="modal-book-id">
|
||||
|
||||
<div>
|
||||
<label for="modal-search-query">Search Google Books:</label>
|
||||
<input type="text" id="modal-search-query">
|
||||
<button id="modal-search-button">Search</button>
|
||||
</div>
|
||||
|
||||
<div id="modal-search-results">
|
||||
<!-- Google Books API search results will appear here -->
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h3>Selected Book Details (Editable)</h3>
|
||||
<div><label>Title: <input type="text" id="modal-edit-title"></label></div>
|
||||
<div><label>Authors: <input type="text" id="modal-edit-authors"></label></div>
|
||||
<div><label>Published Date: <input type="text" id="modal-edit-publishedDate"></label></div>
|
||||
<div><label>ISBN: <input type="text" id="modal-edit-isbn"></label></div>
|
||||
<div><label>Description: <textarea id="modal-edit-description"></textarea></label></div>
|
||||
<div><label>Pages: <input type="number" id="modal-edit-pages"></label></div>
|
||||
<div><label>Publishers: <input type="text" id="modal-edit-publishers"></label></div>
|
||||
<div><label>Subjects: <input type="text" id="modal-edit-subjects"></label></div>
|
||||
<div>
|
||||
<label for="modal-edit-location">Location:</label>
|
||||
<select id="modal-edit-location" name="location_id">
|
||||
<option value="">-- No Location --</option>
|
||||
<!-- Location options will be populated by JavaScript -->
|
||||
</select>
|
||||
</div>
|
||||
<div><label>Cover (Small): <input type="text" id="modal-edit-cover-small"></label></div>
|
||||
<div><label>Cover (Medium): <input type="text" id="modal-edit-cover-medium"></label></div>
|
||||
|
||||
<button id="modal-save-button">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="edit_books.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
405
public/edit_books.js
Normal file
405
public/edit_books.js
Normal file
@ -0,0 +1,405 @@
|
||||
let allLocations = []; // To store fetched locations globally - ADDED HERE
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await fetchLocations(); // Fetch locations on page load
|
||||
const bookListContainer = document.getElementById('book-list-container');
|
||||
const modal = document.getElementById('edit-modal');
|
||||
const closeModalButton = modal.querySelector('.close-button');
|
||||
const modalBookTitle = document.getElementById('modal-book-title');
|
||||
const modalBookIdInput = document.getElementById('modal-book-id');
|
||||
|
||||
const modalSearchQueryInput = document.getElementById('modal-search-query');
|
||||
const modalSearchButton = document.getElementById('modal-search-button');
|
||||
const modalSearchResultsContainer = document.getElementById('modal-search-results');
|
||||
|
||||
const modalEditLocationSelect = document.getElementById('modal-edit-location');
|
||||
const modalEditFields = {
|
||||
title: document.getElementById('modal-edit-title'),
|
||||
authors: document.getElementById('modal-edit-authors'),
|
||||
publishedDate: document.getElementById('modal-edit-publishedDate'),
|
||||
isbn: document.getElementById('modal-edit-isbn'),
|
||||
description: document.getElementById('modal-edit-description'),
|
||||
pages: document.getElementById('modal-edit-pages'),
|
||||
publishers: document.getElementById('modal-edit-publishers'),
|
||||
subjects: document.getElementById('modal-edit-subjects'),
|
||||
cover_small: document.getElementById('modal-edit-cover-small'),
|
||||
cover_medium: document.getElementById('modal-edit-cover-medium')
|
||||
};
|
||||
const modalSaveButton = document.getElementById('modal-save-button');
|
||||
|
||||
let currentBooks = []; // To store the fetched books globally
|
||||
// let allLocations = []; // To store fetched locations globally - REMOVED FROM HERE
|
||||
let currentlyEditingBookId = null;
|
||||
|
||||
const addNewBookButton = document.getElementById('add-new-book-button');
|
||||
|
||||
addNewBookButton.addEventListener('click', () => {
|
||||
currentlyEditingBookId = null; // Signal "add" mode
|
||||
modalBookIdInput.value = ''; // Clear hidden book ID input
|
||||
modalBookTitle.textContent = 'Add New Book'; // Update modal title for adding
|
||||
|
||||
// Clear all form fields
|
||||
Object.values(modalEditFields).forEach(field => field.value = '');
|
||||
modalEditLocationSelect.value = ''; // Reset location dropdown
|
||||
modalSearchResultsContainer.innerHTML = ''; // Clear any previous search results
|
||||
document.getElementById('modal-search-query').value = ''; // Clear search query too
|
||||
|
||||
openModal(); // Open the modal for new entry
|
||||
});
|
||||
|
||||
// Authentication has been disabled for book updates
|
||||
|
||||
// --- Fetch and Display Books ---
|
||||
async function fetchAndDisplayBooks() {
|
||||
try {
|
||||
const response = await fetch('/api/books');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
currentBooks = await response.json();
|
||||
renderBookList(currentBooks);
|
||||
} catch (error) {
|
||||
console.error('Error fetching books:', error);
|
||||
bookListContainer.innerHTML = '<p>Error loading books. See console for details.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderBookList(books) {
|
||||
bookListContainer.innerHTML = ''; // Clear existing list
|
||||
if (books.length === 0) {
|
||||
bookListContainer.innerHTML = '<p>No books found in the library.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const ul = document.createElement('ul');
|
||||
ul.className = 'book-list'; // Add a class for styling if needed
|
||||
books.forEach(book => {
|
||||
const li = document.createElement('li');
|
||||
const locationName = book.Location ? `${book.Location.name}${book.Location.shelf ? ' (Shelf: ' + book.Location.shelf + ')' : ''}` : 'No Location'; // Reverted to book.Location (capital L)
|
||||
li.innerHTML = `
|
||||
<div class="book-entry">
|
||||
<img src="${book.cover_small || 'placeholder.png'}" alt="Cover for ${book.title || 'N/A'}" style="width:50px; height:auto; margin-right:10px; flex-shrink: 0;">
|
||||
<div style="flex-grow: 1;">
|
||||
<p><strong>Title:</strong> ${book.title || 'N/A'}</p>
|
||||
<p><strong>Author(s):</strong> ${book.authors || 'N/A'}</p>
|
||||
<p><strong>Published:</strong> ${book.publishedDate || 'N/A'}</p>
|
||||
<p><strong>ISBN:</strong> ${book.isbn || 'N/A'}</p>
|
||||
<p><strong>Publisher(s):</strong> ${book.publishers || 'N/A'}</p>
|
||||
<p><strong>Location:</strong> ${locationName}</p>
|
||||
</div>
|
||||
<button class="edit-metadata-button" data-book-id="${book.id}" style="margin-left: 10px; align-self: center;">Edit Metadata</button>
|
||||
</div>
|
||||
`;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
bookListContainer.appendChild(ul);
|
||||
|
||||
// Add event listeners to new edit buttons
|
||||
document.querySelectorAll('.edit-metadata-button').forEach(button => {
|
||||
button.addEventListener('click', handleEditMetadataClick);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Modal Logic ---
|
||||
function openModal() {
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modal.style.display = 'none';
|
||||
modalSearchResultsContainer.innerHTML = ''; // Clear search results
|
||||
// Clear form fields
|
||||
Object.values(modalEditFields).forEach(input => input.value = '');
|
||||
modalSearchQueryInput.value = '';
|
||||
}
|
||||
|
||||
closeModalButton.onclick = closeModal;
|
||||
window.onclick = (event) => {
|
||||
if (event.target == modal) {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
async function handleEditMetadataClick(event) {
|
||||
event.preventDefault(); // Prevent default action
|
||||
const bookId = event.target.dataset.bookId;
|
||||
const bookToEdit = currentBooks.find(b => b.id.toString() === bookId);
|
||||
|
||||
if (!bookToEdit) {
|
||||
alert('Book not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
modalBookTitle.textContent = bookToEdit.title;
|
||||
modalBookIdInput.value = bookToEdit.id;
|
||||
currentlyEditingBookId = bookToEdit.id; // Ensure this is set for edit mode
|
||||
|
||||
// Pre-fill search query and existing data
|
||||
modalSearchQueryInput.value = bookToEdit.title || '';
|
||||
if (bookToEdit.isbn) {
|
||||
modalSearchQueryInput.value += ` ISBN: ${bookToEdit.isbn}`;
|
||||
}
|
||||
|
||||
modalEditFields.title.value = bookToEdit.title || '';
|
||||
modalEditFields.authors.value = bookToEdit.authors || '';
|
||||
modalEditFields.publishedDate.value = bookToEdit.publishedDate || '';
|
||||
modalEditFields.isbn.value = bookToEdit.isbn || '';
|
||||
modalEditFields.description.value = bookToEdit.description || '';
|
||||
modalEditFields.pages.value = bookToEdit.number_of_pages || '';
|
||||
modalEditFields.publishers.value = bookToEdit.publishers || '';
|
||||
modalEditFields.subjects.value = bookToEdit.subjects || '';
|
||||
modalEditFields.cover_small.value = bookToEdit.cover_small || '';
|
||||
modalEditFields.cover_medium.value = bookToEdit.cover_medium || '';
|
||||
|
||||
// Populate and set location dropdown
|
||||
modalEditLocationSelect.innerHTML = '<option value="">-- No Location --</option>'; // Clear existing options but keep default
|
||||
allLocations.forEach(loc => {
|
||||
const option = document.createElement('option');
|
||||
option.value = loc.id;
|
||||
option.textContent = `${loc.name}` + (loc.shelf ? ` (Shelf: ${loc.shelf})` : '');
|
||||
modalEditLocationSelect.appendChild(option);
|
||||
});
|
||||
modalEditLocationSelect.value = bookToEdit.location_id || (bookToEdit.Location ? bookToEdit.Location.id : '');
|
||||
|
||||
openModal();
|
||||
}
|
||||
|
||||
// --- Google Books API Search in Modal ---
|
||||
modalSearchButton.addEventListener('click', handleModalSearch);
|
||||
|
||||
async function handleModalSearch() {
|
||||
let originalQuery = modalSearchQueryInput.value.trim();
|
||||
if (!originalQuery) {
|
||||
alert('Please enter a search query.');
|
||||
return;
|
||||
}
|
||||
|
||||
let searchQuery = originalQuery;
|
||||
const potentialIsbn = originalQuery.replace(/-/g, '');
|
||||
let isIsbnQuery = false;
|
||||
|
||||
if (/^(\d{9}[\dX]|\d{13})$/.test(potentialIsbn)) {
|
||||
searchQuery = potentialIsbn; // Use the cleaned ISBN for OpenLibrary
|
||||
isIsbnQuery = true;
|
||||
console.log(`Detected ISBN-like string "${originalQuery}", searching as ISBN "${searchQuery}"`);
|
||||
} else if (originalQuery.toLowerCase().startsWith('isbn:')) {
|
||||
searchQuery = originalQuery.substring(5).replace(/-/g, ''); // Remove 'isbn:' and hyphens
|
||||
isIsbnQuery = true;
|
||||
console.log(`Query "${originalQuery}" formatted for ISBN search, using "${searchQuery}"`);
|
||||
} else {
|
||||
console.log(`Performing general search for "${originalQuery}"`);
|
||||
}
|
||||
|
||||
modalSearchResultsContainer.innerHTML = '<p>Searching...</p>';
|
||||
const searchButton = document.getElementById('modal-search-button');
|
||||
if (searchButton) searchButton.disabled = true;
|
||||
|
||||
let combinedResults = [];
|
||||
|
||||
try {
|
||||
const googleQuery = isIsbnQuery ? `isbn:${searchQuery}` : searchQuery;
|
||||
const googleUrl = `https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent(googleQuery)}&maxResults=5`;
|
||||
const openLibFields = 'key,title,author_name,first_publish_year,isbn,cover_i,publisher,subject,number_of_pages_median,subtitle,first_sentence_value';
|
||||
const openLibUrl = `https://openlibrary.org/search.json?q=${encodeURIComponent(searchQuery)}&limit=5&fields=${openLibFields}`;
|
||||
|
||||
console.log("Searching Google Books API with URL:", googleUrl);
|
||||
console.log("Searching OpenLibrary API with URL:", openLibUrl);
|
||||
|
||||
const [googleResult, openLibResult] = await Promise.allSettled([
|
||||
fetch(googleUrl).then(res => res.ok ? res.json() : Promise.reject(new Error(`Google Books API request failed: ${res.status} ${res.statusText}`))),
|
||||
fetch(openLibUrl).then(res => res.ok ? res.json() : Promise.reject(new Error(`OpenLibrary API request failed: ${res.status} ${res.statusText}`)))
|
||||
]);
|
||||
|
||||
if (googleResult.status === 'fulfilled' && googleResult.value.items) {
|
||||
const googleBooks = googleResult.value.items.map(item => {
|
||||
const volumeInfo = item.volumeInfo;
|
||||
let isbn13 = '', isbn10 = '';
|
||||
if (volumeInfo.industryIdentifiers) {
|
||||
volumeInfo.industryIdentifiers.forEach(id => {
|
||||
if (id.type === 'ISBN_13') isbn13 = id.identifier;
|
||||
if (id.type === 'ISBN_10') isbn10 = id.identifier;
|
||||
});
|
||||
}
|
||||
return {
|
||||
title: volumeInfo.title,
|
||||
authors: volumeInfo.authors ? volumeInfo.authors.join(', ') : '',
|
||||
publishedDate: volumeInfo.publishedDate,
|
||||
isbn: isbn13 || isbn10,
|
||||
description: volumeInfo.description,
|
||||
pageCount: volumeInfo.pageCount,
|
||||
publishers: volumeInfo.publisher ? (Array.isArray(volumeInfo.publisher) ? volumeInfo.publisher.join(', ') : volumeInfo.publisher) : '',
|
||||
subjects: volumeInfo.categories ? volumeInfo.categories.join(', ') : '',
|
||||
cover_small: volumeInfo.imageLinks?.smallThumbnail,
|
||||
cover_medium: volumeInfo.imageLinks?.thumbnail,
|
||||
source: 'Google Books',
|
||||
rawData: volumeInfo
|
||||
};
|
||||
});
|
||||
combinedResults.push(...googleBooks);
|
||||
} else if (googleResult.status === 'rejected') {
|
||||
console.error('Error fetching from Google Books:', googleResult.reason);
|
||||
}
|
||||
|
||||
if (openLibResult.status === 'fulfilled' && openLibResult.value.docs) {
|
||||
const openLibBooks = openLibResult.value.docs.map(doc => {
|
||||
return {
|
||||
title: doc.title,
|
||||
authors: doc.author_name ? doc.author_name.join(', ') : '',
|
||||
publishedDate: doc.first_publish_year ? String(doc.first_publish_year) : '',
|
||||
isbn: doc.isbn ? doc.isbn[0] : '', // Take first ISBN
|
||||
description: doc.first_sentence_value || doc.subtitle || '',
|
||||
pageCount: doc.number_of_pages_median,
|
||||
publishers: doc.publisher ? doc.publisher.join(', ') : '',
|
||||
subjects: doc.subject ? doc.subject.join(', ') : '',
|
||||
cover_small: doc.cover_i ? `https://covers.openlibrary.org/b/id/${doc.cover_i}-S.jpg` : '',
|
||||
cover_medium: doc.cover_i ? `https://covers.openlibrary.org/b/id/${doc.cover_i}-M.jpg` : '',
|
||||
source: 'OpenLibrary',
|
||||
rawData: doc
|
||||
};
|
||||
});
|
||||
combinedResults.push(...openLibBooks);
|
||||
}
|
||||
else if (openLibResult.status === 'rejected') {
|
||||
console.error('Error fetching from OpenLibrary:', openLibResult.reason);
|
||||
}
|
||||
|
||||
displayModalSearchResults(combinedResults);
|
||||
|
||||
} catch (error) { // Catch any unexpected errors during the setup or Promise.allSettled itself
|
||||
console.error('Error in handleModalSearch:', error);
|
||||
modalSearchResultsContainer.innerHTML = `<p>An unexpected error occurred during search: ${error.message}. Please check console.</p>`;
|
||||
} finally {
|
||||
if (searchButton) searchButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function displayModalSearchResults(items) {
|
||||
modalSearchResultsContainer.innerHTML = '';
|
||||
if (!items || items.length === 0) {
|
||||
modalSearchResultsContainer.innerHTML = '<p>No results found from any source.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const ul = document.createElement('ul');
|
||||
items.forEach(bookItem => {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `
|
||||
<strong>${bookItem.title || 'N/A'}</strong> by ${bookItem.authors || 'N/A'} (<em>${bookItem.source}</em>)
|
||||
<br>
|
||||
Published: ${bookItem.publishedDate || 'N/A'}, ISBN: ${bookItem.isbn || 'N/A'}
|
||||
<button class="select-book-result">Use this</button>
|
||||
`;
|
||||
li.querySelector('.select-book-result').addEventListener('click', () => {
|
||||
populateFormWithSelectedBook(bookItem);
|
||||
});
|
||||
ul.appendChild(li);
|
||||
});
|
||||
modalSearchResultsContainer.appendChild(ul);
|
||||
}
|
||||
|
||||
function populateFormWithSelectedBook(bookItem) {
|
||||
modalEditFields.title.value = bookItem.title || '';
|
||||
modalEditFields.authors.value = bookItem.authors || '';
|
||||
modalEditFields.publishedDate.value = bookItem.publishedDate || '';
|
||||
modalEditFields.isbn.value = bookItem.isbn || '';
|
||||
modalEditFields.description.value = bookItem.description || '';
|
||||
modalEditFields.pages.value = bookItem.pageCount || '';
|
||||
modalEditFields.publishers.value = bookItem.publishers || '';
|
||||
modalEditFields.subjects.value = bookItem.subjects || '';
|
||||
modalEditFields.cover_small.value = bookItem.cover_small || '';
|
||||
modalEditFields.cover_medium.value = bookItem.cover_medium || '';
|
||||
}
|
||||
|
||||
// --- Save Changes ---
|
||||
modalSaveButton.addEventListener('click', async () => {
|
||||
const bookData = {
|
||||
title: modalEditFields.title.value.trim(),
|
||||
authors: modalEditFields.authors.value.trim(),
|
||||
publishedDate: modalEditFields.publishedDate.value.trim(),
|
||||
isbn: modalEditFields.isbn.value.trim(),
|
||||
description: modalEditFields.description.value.trim(),
|
||||
number_of_pages: modalEditFields.pages.value ? parseInt(modalEditFields.pages.value, 10) : null,
|
||||
publishers: modalEditFields.publishers.value.trim(),
|
||||
subjects: modalEditFields.subjects.value.trim(),
|
||||
cover_small: modalEditFields.cover_small.value.trim(),
|
||||
cover_medium: modalEditFields.cover_medium.value.trim(),
|
||||
location_id: modalEditLocationSelect.value || null,
|
||||
};
|
||||
|
||||
if (!bookData.title) {
|
||||
alert('Title is required.');
|
||||
return;
|
||||
}
|
||||
|
||||
let url;
|
||||
let method;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
if (currentlyEditingBookId) { // Edit mode
|
||||
url = `/api/books/${currentlyEditingBookId}`;
|
||||
method = 'PUT';
|
||||
// Basic Auth not currently implemented for PUT as per prior setup
|
||||
} else { // Add mode
|
||||
url = '/api/books';
|
||||
method = 'POST';
|
||||
// POST endpoint requires Basic Auth
|
||||
const adminPassword = prompt("Enter admin password to add a new book:");
|
||||
if (adminPassword === null) { // User cancelled prompt
|
||||
return;
|
||||
}
|
||||
headers['Authorization'] = 'Basic ' + btoa('admin:' + adminPassword);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: headers,
|
||||
body: JSON.stringify(bookData)
|
||||
});
|
||||
|
||||
const responseBody = await response.text(); // Read body as text first to handle potential non-JSON errors
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(responseBody);
|
||||
} catch (e) {
|
||||
// If parsing fails, the response was not valid JSON
|
||||
console.error('Failed to parse server response as JSON:', responseBody);
|
||||
throw new Error(`Server returned non-JSON response (status: ${response.status}). Check console for details.`);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = result && result.error ? result.error : `HTTP error! Status: ${response.status}`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
alert(`Book ${method === 'POST' ? 'added' : 'updated'} successfully!`);
|
||||
closeModal();
|
||||
fetchAndDisplayBooks(); // Refresh the list
|
||||
} else {
|
||||
alert(`Failed to ${method === 'POST' ? 'add' : 'update'} book: ${result.error || 'Unknown server error'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error ${method === 'POST' ? 'adding' : 'saving'} book data:`, error);
|
||||
alert(`Error ${method === 'POST' ? 'adding' : 'saving'}: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Initial load
|
||||
fetchAndDisplayBooks();
|
||||
});
|
||||
|
||||
async function fetchLocations() {
|
||||
try {
|
||||
const response = await fetch('/api/locations');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch locations: ${response.status}`);
|
||||
}
|
||||
allLocations = await response.json();
|
||||
console.log('Locations fetched:', allLocations);
|
||||
} catch (error) {
|
||||
console.error('Error fetching locations:', error);
|
||||
// Optionally, inform the user that locations could not be loaded
|
||||
}
|
||||
}
|
||||
22
public/google_api_tester.html
Normal file
22
public/google_api_tester.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Google Books API Tester</title>
|
||||
<link rel="stylesheet" href="style.css"> </head>
|
||||
<body>
|
||||
<h1>Google Books API Tester</h1>
|
||||
|
||||
<div>
|
||||
<label for="search-query">Search Query:</label>
|
||||
<input type="text" id="search-query" placeholder="Enter book title, author, ISBN, etc.">
|
||||
<button id="search-button">Search</button>
|
||||
</div>
|
||||
|
||||
<h2>API Response:</h2>
|
||||
<pre id="api-response"></pre>
|
||||
|
||||
<script src="google_api_tester.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
30
public/google_api_tester.js
Normal file
30
public/google_api_tester.js
Normal file
@ -0,0 +1,30 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const searchButton = document.getElementById('search-button');
|
||||
const searchQueryInput = document.getElementById('search-query');
|
||||
const apiResponseOutput = document.getElementById('api-response');
|
||||
|
||||
searchButton.addEventListener('click', async () => {
|
||||
const query = searchQueryInput.value.trim();
|
||||
if (!query) {
|
||||
apiResponseOutput.textContent = 'Please enter a search query.';
|
||||
return;
|
||||
}
|
||||
|
||||
apiResponseOutput.textContent = 'Searching...';
|
||||
|
||||
try {
|
||||
// Note: For extensive use, you should use an API key.
|
||||
// Add &key=YOUR_API_KEY to the URL if you have one.
|
||||
const response = await fetch(`https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent(query)}`);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
apiResponseOutput.textContent = JSON.stringify(data, null, 2);
|
||||
} catch (error) {
|
||||
console.error('Error fetching from Google Books API:', error);
|
||||
apiResponseOutput.textContent = `Error: ${error.message}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -13,7 +13,9 @@
|
||||
<select id="camera-select"></select>
|
||||
<!-- Confirm mode toggle -->
|
||||
<input type="checkbox" id="confirm-mode">
|
||||
<label for="confirm-mode">Confirm mode</label>s
|
||||
<label for="confirm-mode">Confirm mode</label>
|
||||
<input type="checkbox" id="flash-toggle">
|
||||
<label for="flash-toggle">Flash</label>
|
||||
</div>
|
||||
<div id="interactive" class="viewport"></div>
|
||||
<div>
|
||||
|
||||
137
public/script.js
137
public/script.js
@ -50,9 +50,8 @@ function startScanner() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (quaggaInitialized) {
|
||||
Quagga.start();
|
||||
} else {
|
||||
const initLogic = () => {
|
||||
console.log("startScanner/initLogic: Proceeding with Quagga.init().");
|
||||
Quagga.init({
|
||||
inputStream: {
|
||||
name: "Live",
|
||||
@ -61,34 +60,93 @@ function startScanner() {
|
||||
constraints: {
|
||||
deviceId: selectedDeviceId,
|
||||
facingMode: "environment",
|
||||
// Remove advanced constraints if not needed
|
||||
advanced: [{torch: document.getElementById('flash-toggle') ? document.getElementById('flash-toggle').checked : false}]
|
||||
},
|
||||
},
|
||||
decoder: {
|
||||
readers: ["ean_reader", "ean_8_reader"]
|
||||
}
|
||||
}, function (err) {
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
console.error("Quagga.init() callback: Failed with error:", err);
|
||||
quaggaInitialized = false;
|
||||
alert('Error initializing scanner: ' + err.name + ' - ' + err.message);
|
||||
return;
|
||||
}
|
||||
console.log("Initialization finished. Ready to start");
|
||||
console.log("Quagga.init() callback: Successful. Starting stream...");
|
||||
Quagga.start();
|
||||
console.log("Quagga.init() callback: Quagga.start() called.");
|
||||
quaggaInitialized = true;
|
||||
|
||||
// Set up the onDetected handler
|
||||
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) {
|
||||
if (isScanning) return; // Prevent further scans while a scan is being processed
|
||||
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 ISBN:", isbn);
|
||||
Quagga.stop(); // Stop the scanner once an ISBN is detected
|
||||
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
|
||||
@ -110,20 +168,25 @@ async function fetchBookInfo(isbn) {
|
||||
bookData.isbn2 = isbn; // Add the ISBN to the book data
|
||||
promptUserWithBook(bookData);
|
||||
} else {
|
||||
console.log("No book data found. Restarting scanner...");
|
||||
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.title;
|
||||
const authors = bookData.authors ? bookData.authors.join(', ') : 'Unknown Author';
|
||||
const description = bookData.description || 'No description available';
|
||||
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?`;
|
||||
@ -184,12 +247,13 @@ 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.title) {
|
||||
if (bookData.data && bookData.data.title) {
|
||||
// Display the book information and a success message
|
||||
const title = bookData.title;
|
||||
const authors = bookData.authors ? bookData.authors.join(', ') : 'Unknown Author';
|
||||
const description = bookData.description || 'No description available';
|
||||
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!`;
|
||||
|
||||
@ -201,6 +265,9 @@ async function confirmBookInLibrary(isbn) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -295,4 +362,34 @@ function selectBook(index) {
|
||||
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.
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
110
public/style.css
Normal file
110
public/style.css
Normal file
@ -0,0 +1,110 @@
|
||||
/* Basic Modal Styling */
|
||||
.modal {
|
||||
display: none; /* Hidden by default */
|
||||
position: fixed; /* Stay in place */
|
||||
z-index: 1000; /* Sit on top */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%; /* Full width */
|
||||
height: 100%; /* Full height */
|
||||
overflow: auto; /* Enable scroll if needed */
|
||||
background-color: rgba(0,0,0,0.4); /* Black w/ opacity (backdrop) */
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 10% auto; /* 10% from the top and centered horizontally */
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 80%; /* Could be more or less, depending on screen size */
|
||||
max-width: 700px; /* Max width for larger screens */
|
||||
border-radius: 8px;
|
||||
position: relative; /* For positioning the close button */
|
||||
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);
|
||||
}
|
||||
|
||||
/* Close Button */
|
||||
.close-button {
|
||||
color: #aaa;
|
||||
float: right; /* Position to the top-right */
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
position: absolute; /* Position relative to modal-content */
|
||||
top: 10px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.close-button:hover,
|
||||
.close-button:focus {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Style for the book list entries on edit_books.html for better layout */
|
||||
#book-list-container ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#book-list-container li {
|
||||
border: 1px solid #ddd;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.book-entry {
|
||||
display: flex; /* Use flexbox for alignment */
|
||||
align-items: flex-start; /* Align items to the start of the cross axis */
|
||||
}
|
||||
|
||||
.book-entry img {
|
||||
margin-right: 15px;
|
||||
flex-shrink: 0; /* Prevent image from shrinking */
|
||||
}
|
||||
|
||||
.book-entry div {
|
||||
flex-grow: 1; /* Allow text content to take up remaining space */
|
||||
}
|
||||
|
||||
.book-entry button {
|
||||
margin-left: 10px;
|
||||
align-self: center; /* Center button vertically within the flex container */
|
||||
}
|
||||
|
||||
/* Ensure modal input fields are a bit more organized */
|
||||
.modal-content div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.modal-content label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.modal-content input[type="text"],
|
||||
.modal-content input[type="number"],
|
||||
.modal-content textarea {
|
||||
width: calc(100% - 22px); /* Adjust width to account for padding/border */
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box; /* Include padding and border in the element's total width and height */
|
||||
}
|
||||
.modal-content textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
.modal-content button {
|
||||
padding: 10px 15px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.modal-content button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
#modal-search-button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user