mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-10-05 17:11:11 +01:00
[Feature] Select multiple books to queue (#57)
Related to #34 This PR adds: - Additional column for checkboxes as the first column to select which books to download - Select/deselect all books - Button and modal to confirm download of selected books Maybe the checkbox column should be at the end of the table, next to actions?   
This commit is contained in:
+139
-19
@@ -4,6 +4,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const elements = {
|
||||
searchInput: document.getElementById('search-input'),
|
||||
searchButton: document.getElementById('search-button'),
|
||||
selectAllCheckbox: document.getElementById('select-all-checkbox'),
|
||||
downloadSelectedButton: document.getElementById('download-selected-button'),
|
||||
resultsSectionAccordion: document.getElementById('results-section-accordion'),
|
||||
searchAccordion: document.getElementById('search-accordion'),
|
||||
resultsHeading: document.getElementById('results-heading'),
|
||||
@@ -18,7 +20,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
};
|
||||
|
||||
// State
|
||||
let currentBookDetails = null;
|
||||
let modalDetails = null;
|
||||
const selectedBooks = new Set();
|
||||
const STATE = {
|
||||
isSearching: false,
|
||||
isLoadingDetails: false
|
||||
@@ -110,6 +113,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const text = cell ? cell.textContent.trim() : '';
|
||||
if (columnName === 'size') return parseFileSize(text);
|
||||
if (columnName === 'title') return parseTitle(text);
|
||||
return text;
|
||||
};
|
||||
|
||||
rows.sort((a, b) => {
|
||||
@@ -139,6 +143,89 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (icon) {
|
||||
icon.setAttribute('uk-icon', `icon: ${order === 'asc' ? 'triangle-up' : 'triangle-down'}`);
|
||||
}
|
||||
},
|
||||
|
||||
updateDownloadSelectedButton() {
|
||||
elements.downloadSelectedButton.disabled = selectedBooks.size === 0;
|
||||
},
|
||||
|
||||
handleCheckboxChange(event) {
|
||||
const checkbox = event.target;
|
||||
if (checkbox.checked) {
|
||||
selectedBooks.add(checkbox.value);
|
||||
} else {
|
||||
selectedBooks.delete(checkbox.value);
|
||||
}
|
||||
utils.updateDownloadSelectedButton();
|
||||
},
|
||||
|
||||
async handleDownloadSelected() {
|
||||
if (selectedBooks.size === 0) return;
|
||||
|
||||
const bookIds = Array.from(selectedBooks);
|
||||
const books = bookIds.map((bookId) => {
|
||||
const row = document.querySelector(`#book-${bookId}`).closest('tr');
|
||||
return {
|
||||
title: row.querySelector('td:nth-child(4)').textContent,
|
||||
author: row.querySelector('td:nth-child(5)').textContent
|
||||
};
|
||||
});
|
||||
|
||||
// Confirmation modal
|
||||
const confirmationContent = `
|
||||
<h2>Confirm Download</h2>
|
||||
<p>Are you sure you want to download ${books.length} book${books.length > 1 ? 's' : ''}?</p>
|
||||
<div class="uk-overflow-auto">
|
||||
<table class="uk-table uk-table-divider uk-table-small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Author</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${books.map((book) => `
|
||||
<tr>
|
||||
<td>${book.title}</td>
|
||||
<td>${book.author}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="uk-flex uk-flex-between uk-margin-top">
|
||||
<button id="cancel-download" class="uk-button uk-button-default">Cancel</button>
|
||||
<button id="confirm-download" class="uk-button uk-button-primary">Download</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
elements.detailsContainer.innerHTML = confirmationContent;
|
||||
|
||||
document.getElementById('cancel-download').addEventListener('click', modal.close);
|
||||
document.getElementById('confirm-download').addEventListener('click', async () => {
|
||||
await utils.confirmDownload(bookIds);
|
||||
});
|
||||
|
||||
modal.open();
|
||||
},
|
||||
|
||||
async confirmDownload(bookIds) {
|
||||
bookIds.map((bookId) =>
|
||||
utils.fetchJson(`${API_ENDPOINTS.download}?id=${encodeURIComponent(bookId)}`)
|
||||
);
|
||||
|
||||
// Uncheck all selected checkboxes
|
||||
selectedBooks.forEach((bookId) => {
|
||||
const checkbox = document.getElementById(`book-${bookId}`);
|
||||
if (checkbox) checkbox.checked = false;
|
||||
});
|
||||
|
||||
const selectAllCheckbox = document.getElementById('select-all-checkbox');
|
||||
if (selectAllCheckbox) selectAllCheckbox.checked = false;
|
||||
|
||||
selectedBooks.clear();
|
||||
utils.updateDownloadSelectedButton();
|
||||
modal.close();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -146,19 +233,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const search = {
|
||||
async performSearch(query) {
|
||||
if (STATE.isSearching) return;
|
||||
|
||||
|
||||
try {
|
||||
STATE.isSearching = true;
|
||||
utils.showLoading(elements.searchLoading);
|
||||
|
||||
if (!elements.searchAccordion.classList.contains('uk-open'))
|
||||
{
|
||||
if (!elements.searchAccordion.classList.contains('uk-open')) {
|
||||
utils.showAccordion(elements.resultsSectionAccordion);
|
||||
};
|
||||
const data = await utils.fetchJson(
|
||||
`${API_ENDPOINTS.search}?query=${encodeURIComponent(query)}`
|
||||
);
|
||||
|
||||
|
||||
this.displayResults(data);
|
||||
} catch (error) {
|
||||
this.handleSearchError(error);
|
||||
@@ -170,7 +256,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
displayResults(books) {
|
||||
elements.resultsTableBody.innerHTML = '';
|
||||
|
||||
|
||||
if (!books.length) {
|
||||
this.displayNoResults();
|
||||
return;
|
||||
@@ -193,7 +279,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
},
|
||||
|
||||
createBookRow(book, index) {
|
||||
const checkboxCell = utils.createElement('td', {}, [
|
||||
utils.createElement('input', {
|
||||
type: 'checkbox',
|
||||
className: 'uk-checkbox',
|
||||
id: 'book-' + book.id,
|
||||
name: 'book-' + book.id,
|
||||
value: book.id,
|
||||
onchange: utils.handleCheckboxChange
|
||||
})
|
||||
]);
|
||||
|
||||
return utils.createElement('tr', {}, [
|
||||
checkboxCell,
|
||||
utils.createElement('td', { textContent: index + 1 }),
|
||||
this.createPreviewCell(book.preview),
|
||||
utils.createElement('td', { textContent: book.title || 'N/A' }),
|
||||
@@ -261,8 +359,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const book = await utils.fetchJson(
|
||||
`${API_ENDPOINTS.info}?id=${encodeURIComponent(bookId)}`
|
||||
);
|
||||
|
||||
currentBookDetails = book;
|
||||
|
||||
modalDetails = book;
|
||||
this.displayDetails(book);
|
||||
} catch (error) {
|
||||
this.handleDetailsError(error);
|
||||
@@ -273,7 +371,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
displayDetails(book) {
|
||||
elements.detailsContainer.innerHTML = this.generateDetailsHTML(book);
|
||||
|
||||
|
||||
// Add event listeners
|
||||
document.getElementById('download-button')
|
||||
.addEventListener('click', () => this.downloadBook(book));
|
||||
@@ -333,7 +431,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
await utils.fetchJson(
|
||||
`${API_ENDPOINTS.download}?id=${encodeURIComponent(book.id)}`
|
||||
);
|
||||
|
||||
|
||||
modal.close();
|
||||
status.fetch();
|
||||
} catch (error) {
|
||||
@@ -372,7 +470,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
display(data) {
|
||||
elements.statusTableBody.innerHTML = '';
|
||||
|
||||
|
||||
// Handle each status type
|
||||
Object.entries(data).forEach(([status, booksInStatus]) => {
|
||||
// If the status section has books
|
||||
@@ -417,7 +515,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
createPreviewCell(previewUrl) {
|
||||
const cell = utils.createElement('td');
|
||||
|
||||
|
||||
if (previewUrl) {
|
||||
const img = utils.createElement('img', {
|
||||
src: previewUrl,
|
||||
@@ -428,14 +526,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
} else {
|
||||
cell.textContent = 'N/A';
|
||||
}
|
||||
|
||||
|
||||
return cell;
|
||||
},
|
||||
|
||||
handleError(error) {
|
||||
console.error('Status error:', error);
|
||||
elements.statusTableBody.innerHTML = '';
|
||||
|
||||
|
||||
const errorRow = utils.createElement('tr', {}, [
|
||||
utils.createElement('td', {
|
||||
colSpan: '4',
|
||||
@@ -443,7 +541,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
textContent: 'Error loading status. Will retry automatically.'
|
||||
})
|
||||
]);
|
||||
|
||||
|
||||
elements.statusTableBody.appendChild(errorRow);
|
||||
}
|
||||
};
|
||||
@@ -456,7 +554,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
close() {
|
||||
elements.modalOverlay.classList.remove('active');
|
||||
currentBookDetails = null;
|
||||
modalDetails = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -489,19 +587,41 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
modal.close();
|
||||
}
|
||||
});
|
||||
// Download selected books
|
||||
elements.downloadSelectedButton.addEventListener('click', utils.handleDownloadSelected);
|
||||
|
||||
// Check/uncheck all book checkboxes
|
||||
elements.selectAllCheckbox.addEventListener('change', (event) => {
|
||||
const isChecked = event.target.checked;
|
||||
|
||||
document.querySelectorAll('.uk-checkbox').forEach((checkbox) => {
|
||||
if (checkbox !== elements.selectAllCheckbox) {
|
||||
checkbox.checked = isChecked;
|
||||
if (isChecked) {
|
||||
selectedBooks.add(checkbox.value);
|
||||
} else {
|
||||
selectedBooks.delete(checkbox.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
utils.updateDownloadSelectedButton();
|
||||
});
|
||||
|
||||
function setupSorting() {
|
||||
const headers = document.querySelectorAll('#results-table thead th');
|
||||
headers.forEach((header, index) => {
|
||||
const headers = document.querySelectorAll('#results-table thead th[data-sort]');
|
||||
headers.forEach((header) => {
|
||||
let sortOrder = 'asc';
|
||||
header.addEventListener('click', () => {
|
||||
utils.sortResultsTable(index, sortOrder);
|
||||
const allHeaders = Array.from(document.querySelectorAll('#results-table thead th'));
|
||||
const columnIndex = allHeaders.indexOf(header);
|
||||
utils.sortResultsTable(columnIndex, sortOrder);
|
||||
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupSorting();
|
||||
|
||||
}
|
||||
|
||||
// Initialize
|
||||
|
||||
@@ -43,10 +43,14 @@
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
<div class="results-content">
|
||||
<button class="uk-button uk-button-primary uk-margin-small" id="download-selected-button" disabled>Download Selected</button>
|
||||
<div class="uk-overflow-auto">
|
||||
<table id="results-table" class="uk-table uk-table-hover uk-table-divider" role="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input type="checkbox" id="select-all-checkbox" class="uk-checkbox" />
|
||||
</th>
|
||||
<th scope="col" data-sort="index"># <span class="sort-icon" uk-icon></span></th>
|
||||
<th scope="col">Preview</th>
|
||||
<th scope="col" data-sort="title">Title <span class="sort-icon" uk-icon></span></th>
|
||||
@@ -69,7 +73,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Book Details Modal -->
|
||||
<!-- Modal -->
|
||||
<div class="modal-overlay" id="modal-overlay" role="dialog" aria-modal="true">
|
||||
<div class="details-container" id="details-container">
|
||||
<!-- Details will be dynamically injected here -->
|
||||
|
||||
Reference in New Issue
Block a user