From 0034a688c42d17bd6e9378e280a9ead812f5bb39 Mon Sep 17 00:00:00 2001 From: FunnyPocketBook Date: Mon, 13 Jan 2025 22:37:35 +0100 Subject: [PATCH] [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) --- static/js/main.js | 158 +++++++++++++++++++++++++++++++++++++------ templates/index.html | 6 +- 2 files changed, 144 insertions(+), 20 deletions(-) diff --git a/static/js/main.js b/static/js/main.js index 233a6357..65032674 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -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 = ` +

Confirm Download

+

Are you sure you want to download ${books.length} book${books.length > 1 ? 's' : ''}?

+
+ + + + + + + + + ${books.map((book) => ` + + + + + `).join('')} + +
TitleAuthor
${book.title}${book.author}
+
+
+ + +
+ `; + + 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 diff --git a/templates/index.html b/templates/index.html index 6f8761b0..4c38e39e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -43,10 +43,14 @@ Loading...
+
+ @@ -69,7 +73,7 @@ - +
+ + # Preview Title