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
|
// Fetch book from the local database by ISBN
|
||||||
const fetchBookFromLocalDatabase = async (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
|
// 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'],
|
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);
|
res.json(books);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch books with images:', 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
|
// Confirm book is in the library by ISBN
|
||||||
app.get('/book/confirm/:isbn', async (req, res) => {
|
app.get('/book/confirm/:isbn', async (req, res) => {
|
||||||
const { isbn } = req.params;
|
const { isbn } = req.params;
|
||||||
console.log(`Fetching book data for ISBN: ${isbn}`);
|
console.log(`Cascade: /book/confirm/:isbn - Confirming ISBN: ${isbn}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if the book is in the local database
|
// Check if the book is in the local database
|
||||||
const localBook = await fetchBookFromLocalDatabase(isbn);
|
const localBook = await fetchBookFromLocalDatabase(isbn);
|
||||||
|
|
||||||
if (localBook) {
|
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 });
|
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) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error('Cascade: /book/confirm/:isbn - Error during lookup:', error);
|
||||||
return res.status(500).json({ error: 'Failed to fetch book data' });
|
return res.status(500).json({ error: 'Failed to confirm book in local library' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
app.get('/search-title', async (req, res) => {
|
app.get('/search-title', async (req, res) => {
|
||||||
const { title, internalOnly = false } = req.query;
|
const { title, internalOnly = false } = req.query;
|
||||||
console.log(`Searching for books by title or related fields: ${title}`);
|
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 {
|
try {
|
||||||
const locations = await Location.findAll();
|
const locations = await Location.findAll({
|
||||||
|
order: [
|
||||||
|
['name', 'ASC'],
|
||||||
|
['shelf', 'ASC']
|
||||||
|
]
|
||||||
|
});
|
||||||
res.json(locations);
|
res.json(locations);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch locations:', 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);
|
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 { isbn } = req.params;
|
||||||
const googleBooksApiKey = process.env.GOOGLE_BOOKS_API_KEY;
|
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>
|
<select id="camera-select"></select>
|
||||||
<!-- Confirm mode toggle -->
|
<!-- Confirm mode toggle -->
|
||||||
<input type="checkbox" id="confirm-mode">
|
<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>
|
||||||
<div id="interactive" class="viewport"></div>
|
<div id="interactive" class="viewport"></div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
137
public/script.js
137
public/script.js
@ -50,9 +50,8 @@ function startScanner() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (quaggaInitialized) {
|
const initLogic = () => {
|
||||||
Quagga.start();
|
console.log("startScanner/initLogic: Proceeding with Quagga.init().");
|
||||||
} else {
|
|
||||||
Quagga.init({
|
Quagga.init({
|
||||||
inputStream: {
|
inputStream: {
|
||||||
name: "Live",
|
name: "Live",
|
||||||
@ -61,34 +60,93 @@ function startScanner() {
|
|||||||
constraints: {
|
constraints: {
|
||||||
deviceId: selectedDeviceId,
|
deviceId: selectedDeviceId,
|
||||||
facingMode: "environment",
|
facingMode: "environment",
|
||||||
// Remove advanced constraints if not needed
|
advanced: [{torch: document.getElementById('flash-toggle') ? document.getElementById('flash-toggle').checked : false}]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
decoder: {
|
decoder: {
|
||||||
readers: ["ean_reader", "ean_8_reader"]
|
readers: ["ean_reader", "ean_8_reader"]
|
||||||
}
|
}
|
||||||
}, function (err) {
|
}, function(err) {
|
||||||
if (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;
|
return;
|
||||||
}
|
}
|
||||||
console.log("Initialization finished. Ready to start");
|
console.log("Quagga.init() callback: Successful. Starting stream...");
|
||||||
Quagga.start();
|
Quagga.start();
|
||||||
|
console.log("Quagga.init() callback: Quagga.start() called.");
|
||||||
quaggaInitialized = true;
|
quaggaInitialized = true;
|
||||||
|
Quagga.offDetected(processBarcode); // Remove before adding to prevent duplicates
|
||||||
// Set up the onDetected handler
|
|
||||||
Quagga.onDetected(processBarcode);
|
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) {
|
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
|
isScanning = true; // Set the scanning flag
|
||||||
|
console.log("Cascade: processBarcode - Set isScanning to true.");
|
||||||
|
|
||||||
const isbn = data.codeResult.code;
|
const isbn = data.codeResult.code;
|
||||||
console.log("Detected ISBN:", isbn);
|
console.log("Detected code:", isbn);
|
||||||
Quagga.stop(); // Stop the scanner once an ISBN is detected
|
|
||||||
|
// 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) {
|
if (document.getElementById('confirm-mode').checked) {
|
||||||
// Confirm book in library
|
// Confirm book in library
|
||||||
@ -110,20 +168,25 @@ async function fetchBookInfo(isbn) {
|
|||||||
bookData.isbn2 = isbn; // Add the ISBN to the book data
|
bookData.isbn2 = isbn; // Add the ISBN to the book data
|
||||||
promptUserWithBook(bookData);
|
promptUserWithBook(bookData);
|
||||||
} else {
|
} 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
|
startScanner(); // Restart the scanner if no book information is found
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching book data:', 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
|
startScanner(); // Restart the scanner on error as well
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function promptUserWithBook(bookData) {
|
function promptUserWithBook(bookData) {
|
||||||
// Prepare the information to display in the confirm dialog
|
// Prepare the information to display in the confirm dialog
|
||||||
const title = bookData.title;
|
const title = bookData.data.title;
|
||||||
const authors = bookData.authors ? bookData.authors.join(', ') : 'Unknown Author';
|
const authors = bookData.data.authors ? (Array.isArray(bookData.data.authors) ? bookData.data.authors.join(', ') : bookData.data.authors) : 'Unknown Author';
|
||||||
const description = bookData.description || 'No description available';
|
const description = bookData.data.description || 'No description available';
|
||||||
|
|
||||||
// Combine the information into a single message
|
// 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?`;
|
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 {
|
try {
|
||||||
const response = await fetch(`/book/confirm/${isbn}`);
|
const response = await fetch(`/book/confirm/${isbn}`);
|
||||||
const bookData = await response.json();
|
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
|
// Display the book information and a success message
|
||||||
const title = bookData.title;
|
const title = bookData.data.title;
|
||||||
const authors = bookData.authors ? bookData.authors.join(', ') : 'Unknown Author';
|
const authors = bookData.data.authors ? (Array.isArray(bookData.data.authors) ? bookData.data.authors.join(', ') : bookData.data.authors) : 'Unknown Author';
|
||||||
const description = bookData.description || 'No description available';
|
const description = bookData.data.description || 'No description available';
|
||||||
|
|
||||||
const message = `Title: ${title}\nAuthor(s): ${authors}\nDescription: ${description}\n\nBook found in the library!`;
|
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);
|
console.error('Error confirming book in library:', error);
|
||||||
alert('An error occurred while confirming the book in the library.');
|
alert('An error occurred while confirming the book in the library.');
|
||||||
} finally {
|
} 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
|
startScanner(); // Restart the scanner after processing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -295,4 +362,34 @@ function selectBook(index) {
|
|||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
getCameras();
|
getCameras();
|
||||||
searchByTitle('');
|
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