[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?


![image](https://github.com/user-attachments/assets/459934cd-2503-41bd-83d7-b0517a541062)


![image](https://github.com/user-attachments/assets/77ff4636-6802-441e-a02c-17dafb6bfa9c)


![image](https://github.com/user-attachments/assets/4648357d-26e4-4be8-a5ca-19444486ceef)
This commit is contained in:
FunnyPocketBook
2025-01-13 16:37:35 -05:00
committed by GitHub
parent 39ac60eaab
commit 0034a688c4
2 changed files with 144 additions and 20 deletions
+139 -19
View File
@@ -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
+5 -1
View File
@@ -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 -->