Files
shelfmark/static/js/main.js
T
Stephon ParkerandCaliBrain 5e04b6bfb8 Add Download Queue Management & Concurrent Downloads (#231)
## Summary

This PR completely overhauls the download system to support concurrent
downloads, cancellation, and advanced queue
management. No more waiting for stuck downloads to block the entire
queue!

  ## New Features

  ###  **Concurrent Downloads**
  - **3 simultaneous downloads** by default (configurable)
- ThreadPoolExecutor-based architecture replaces single-threaded
bottleneck
  - Downloads no longer block each other

  ###  **Download Cancellation**
  - **Cancel button** for active/queued downloads
  - Clean cancellation with partial file cleanup
  - Thread-safe cancellation flags

  ### **Queue Priority & Reordering**
  - **Priority-based queue** (lower number = higher priority)
  - **Editable priority inputs** in UI for queued items
  - **Bulk reordering** API support

  ## UI Enhancements

- **Enhanced status table** with Priority, Progress, and Actions columns
  - **Progress bars** for active downloads
  - **Cancel buttons** for downloads
- **Queue management controls**: Refresh, Clear Completed, Active
Counter
  - **Real-time updates** every 60 seconds

## Implementation

  ### New Architecture
  - `queue.PriorityQueue` replaces simple `set()` for proper ordering
  - `concurrent.futures.ThreadPoolExecutor` handles worker threads
  - `threading.Event` flags enable clean cancellation
  - Thread-safe operations with proper locking

  ### New API Endpoints
  DELETE /api/download/{id}/cancel     # Cancel downloads
PUT /api/queue/{id}/priority # Set priorityPOST /api/queue/reorder #
Bulk reorder
  GET /api/downloads/active          # Active tracking
  DELETE /api/queue/clear            # Clear completed

  ### New Environment Variables
  ```bash
  MAX_CONCURRENT_DOWNLOADS=3          # Worker thread count
  DOWNLOAD_PROGRESS_UPDATE_INTERVAL=5  # Progress frequency
  ```

### Fully backward compatible - existing functionality unchanged

### Fixes

  - Resolves: Downloads blocking when one gets stuck
  - Resolves: No way to cancel problematic downloads
  - Resolves: No queue management or prioritization
  
  #183

---------

Co-authored-by: CaliBrain <calibrain@l4n.xyz>
2025-08-25 13:28:35 -04:00

1079 lines
41 KiB
JavaScript

// Main application JavaScript
document.addEventListener('DOMContentLoaded', () => {
// DOM Elements
const elements = {
search: {
searchInput: document.getElementById('search-input'),
searchButton: document.getElementById('search-button'),
advanced: {
advSearchButton: document.getElementById('adv-search-button'),
searchFiltersForm: document.getElementById('search-filters')
}
},
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'),
resultsTable: document.getElementById('results-table'),
resultsTableBody: document.querySelector('#results-table tbody'),
searchLoading: document.getElementById('search-loading'),
statusLoading: document.getElementById('status-loading'),
statusTable: document.getElementById('status-table'),
statusTableBody: document.querySelector('#status-table tbody'),
modalOverlay: document.getElementById('modal-overlay'),
detailsContainer: document.getElementById('details-container'),
theme: {
toggle: document.getElementById('theme-toggle'),
text: document.getElementById('theme-text'),
dropdown: document.querySelector('[uk-dropdown]'),
},
debug: {
form: document.getElementById('debug-form'),
button: document.getElementById('debug-button'),
spinner: document.getElementById('debug-spinner')
},
restart: {
form: document.getElementById('restart-form'),
button: document.getElementById('restart-button'),
spinner: document.getElementById('restart-spinner')
}
};
// State
let modalDetails = null;
const selectedBooks = new Set();
const STATE = {
isSearching: false,
isLoadingDetails: false,
};
// Constants
const REFRESH_INTERVAL = 60000; // 60 seconds
const API_ENDPOINTS = {
search: '/request/api/search',
info: '/request/api/info',
download: '/request/api/download',
status: '/request/api/status',
cancelDownload: '/request/api/download',
setPriority: '/request/api/queue',
reorderQueue: '/request/api/queue/reorder',
queueOrder: '/request/api/queue/order',
activeDownloads: '/request/api/downloads/active',
clearCompleted: '/request/api/queue/clear'
};
const FILTERS = ['isbn', 'author', 'title', 'lang' , 'sort', "content", "format"];
// Utility Functions
const utils = {
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
showLoading(element) {
element.removeAttribute('hidden');
},
hideLoading(element) {
element.setAttribute('hidden', '');
},
showAccordion(element) {
UIkit.accordion(element).toggle(1, true);
},
async fetchJson(url, options = {}) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
},
createElement(tag, attributes = {}, children = []) {
const element = document.createElement(tag);
Object.entries(attributes).forEach(([key, value]) => {
element[key] = value;
});
children.forEach(child => {
if (typeof child === 'string') {
element.appendChild(document.createTextNode(child));
} else {
element.appendChild(child);
}
});
return element;
},
sortResultsTable(column, order = 'asc') {
const rows = Array.from(elements.resultsTableBody.querySelectorAll('tr'));
const headers = document.querySelectorAll('#results-table thead th');
const parseFileSize = (size) => {
const match = size.match(/([\d.]+)([KMGT]B)/i);
if (!match) return 0;
const [_, value, unit] = match;
const multiplier = { KB: 1, MB: 1024, GB: 1024 * 1024, TB: 1024 * 1024 * 1024 };
return parseFloat(value) * (multiplier[unit.toUpperCase()] || 1);
};
const parseTitle = (title) => {
return title.replace(/^(The)\s+/i, '').trim();
}
const columnName = headers[column].textContent.trim().toLowerCase();
const getCellValue = (row, column) => {
const cell = row.querySelector(`td:nth-child(${column + 1})`);
const text = cell ? cell.textContent.trim() : '';
if (columnName === 'size') return parseFileSize(text);
if (columnName === 'title') return parseTitle(text);
return text;
};
rows.sort((a, b) => {
const valA = getCellValue(a, column);
const valB = getCellValue(b, column);
if (!isNaN(valA) && !isNaN(valB)) {
return order === 'asc' ? valA - valB : valB - valA;
}
return order === 'asc'
? valA.localeCompare(valB)
: valB.localeCompare(valA);
});
elements.resultsTableBody.innerHTML = '';
rows.forEach(row => elements.resultsTableBody.appendChild(row));
headers.forEach(header => {
const icon = header.querySelector('.sort-icon');
if (icon) {
icon.removeAttribute('uk-icon');
}
});
const currentHeader = headers[column];
const icon = currentHeader.querySelector('.sort-icon');
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, priority = 0) {
try {
await Promise.all(bookIds.map((bookId) =>
utils.fetchJson(`${API_ENDPOINTS.download}?id=${encodeURIComponent(bookId)}&priority=${priority}`)
));
UIkit.notification({
message: `Successfully queued ${bookIds.length} book${bookIds.length > 1 ? 's' : ''} for download!`,
status: 'success',
timeout: 3000
});
status.fetch();
} catch (error) {
console.error('Download error:', error);
UIkit.notification({
message: 'Failed to queue some books for download.',
status: 'danger',
timeout: 3000
});
}
this.clearAllCheckboxes();
modal.close();
},
clearAllCheckboxes() {
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();
}
};
// Queue Management Functions
const queue = {
async cancelDownload(bookId) {
try {
const response = await fetch(`${API_ENDPOINTS.cancelDownload}/${bookId}/cancel`, {
method: 'DELETE'
});
if (response.ok) {
UIkit.notification({
message: 'Download cancelled successfully',
status: 'success',
timeout: 2000
});
status.fetch();
this.updateActiveDownloads();
} else {
throw new Error('Failed to cancel download');
}
} catch (error) {
console.error('Cancel download error:', error);
UIkit.notification({
message: 'Failed to cancel download',
status: 'danger',
timeout: 3000
});
}
},
async setPriority(bookId, priority) {
try {
const response = await fetch(`${API_ENDPOINTS.setPriority}/${bookId}/priority`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ priority })
});
if (response.ok) {
UIkit.notification({
message: `Priority updated to ${priority}`,
status: 'success',
timeout: 2000
});
status.fetch();
} else {
throw new Error('Failed to update priority');
}
} catch (error) {
console.error('Set priority error:', error);
UIkit.notification({
message: 'Failed to update priority',
status: 'danger',
timeout: 3000
});
}
},
async clearCompleted() {
try {
const response = await fetch(API_ENDPOINTS.clearCompleted, {
method: 'DELETE'
});
if (response.ok) {
const data = await response.json();
UIkit.notification({
message: `Cleared ${data.removed_count} completed items`,
status: 'success',
timeout: 2000
});
status.fetch();
} else {
throw new Error('Failed to clear completed items');
}
} catch (error) {
console.error('Clear completed error:', error);
UIkit.notification({
message: 'Failed to clear completed items',
status: 'danger',
timeout: 3000
});
}
},
async updateActiveDownloads() {
try {
const data = await utils.fetchJson(API_ENDPOINTS.activeDownloads);
const count = data.active_downloads ? data.active_downloads.length : 0;
const element = document.getElementById('active-downloads-count');
if (element) {
element.textContent = `Active: ${count}`;
}
} catch (error) {
console.error('Update active downloads error:', error);
}
}
};
// Search Functions
const search = {
async performSearch(query) {
utils.clearAllCheckboxes();
if (STATE.isSearching) return;
try {
STATE.isSearching = true;
utils.showLoading(elements.searchLoading);
if (!elements.searchAccordion.classList.contains('uk-open')) {
utils.showAccordion(elements.resultsSectionAccordion);
};
const data = await utils.fetchJson(
`${API_ENDPOINTS.search}?${query}`
);
this.displayResults(data);
} catch (error) {
this.handleSearchError(error);
} finally {
STATE.isSearching = false;
utils.hideLoading(elements.searchLoading);
}
},
buildQuery() {
let queryParams = [];
if(elements.search.searchInput.value.trim()) {
queryParams.push(`query=${encodeURIComponent(elements.search.searchInput.value.trim())}`);
}
if(elements.search.advanced.searchFiltersForm.hasAttribute('hidden')) {
//Not advanced search
return queryParams.join('&');
}
FILTERS.forEach(filterType => {
if (filterType === 'format') {
// Handle format checkboxes
const checkboxes = document.querySelectorAll(`[id^="${filterType}-"]:checked`);
checkboxes.forEach(checkbox => {
queryParams.push(`${filterType}=${encodeURIComponent(checkbox.value)}`);
});
} else {
const inputs = document.querySelectorAll(`[id^="${filterType}-input"]`);
inputs.forEach(input => {
const value = input.value.trim();
if (value) {
queryParams.push(`${filterType}=${encodeURIComponent(value.trim())}`);
}
});
}
});
return queryParams.join('&');
},
displayResults(books) {
elements.resultsTableBody.innerHTML = '';
if (!books.length) {
this.displayNoResults();
return;
}
books.forEach((book, index) => {
const row = this.createBookRow(book, index);
elements.resultsTableBody.appendChild(row);
});
},
displayNoResults() {
const row = utils.createElement('tr', {}, [
utils.createElement('td', {
colSpan: '10',
textContent: 'No results found.'
})
]);
elements.resultsTableBody.appendChild(row);
},
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' }),
utils.createElement('td', { textContent: book.author || 'N/A' }),
utils.createElement('td', { textContent: book.publisher || 'N/A' }),
utils.createElement('td', { textContent: book.year || 'N/A' }),
utils.createElement('td', { textContent: book.language || 'N/A' }),
utils.createElement('td', { textContent: book.format || 'N/A' }),
utils.createElement('td', { textContent: book.size || 'N/A' }),
this.createActionCell(book)
]);
},
createPreviewCell(previewUrl) {
if (!previewUrl) {
return utils.createElement('td', { textContent: 'N/A' });
}
const img = utils.createElement('img', {
src: previewUrl,
alt: 'Book Preview',
style: 'max-width: 60px;'
});
return utils.createElement('td', {}, [img]);
},
createActionCell(book) {
const buttonDetails = utils.createElement('button', {
className: 'uk-button uk-button-default uk-align-center uk-margin-small uk-width-1-1',
onclick: () => bookDetails.show(book.id)
}, [utils.createElement('span', { textContent: 'Details' })]);
const downloadButton = utils.createElement('button', {
className: 'uk-button uk-button-primary uk-align-center uk-margin-small uk-width-1-1',
onclick: () => bookDetails.downloadBook(book)
}, [utils.createElement('span', { textContent: 'Download' })]);
return utils.createElement('td', {}, [buttonDetails, downloadButton]);
},
handleSearchError(error) {
console.error('Search error:', error);
elements.resultsTableBody.innerHTML = '';
const errorRow = utils.createElement('tr', {}, [
utils.createElement('td', {
colSpan: '10',
textContent: 'An error occurred while searching. Please try again.'
})
]);
elements.resultsTableBody.appendChild(errorRow);
}
};
// Book Details Functions
const bookDetails = {
async show(bookId) {
if (STATE.isLoadingDetails) return;
try {
STATE.isLoadingDetails = true;
modal.open();
elements.detailsContainer.innerHTML = '<p>Loading details...</p>';
const book = await utils.fetchJson(
`${API_ENDPOINTS.info}?id=${encodeURIComponent(bookId)}`
);
modalDetails = book;
this.displayDetails(book);
} catch (error) {
this.handleDetailsError(error);
} finally {
STATE.isLoadingDetails = false;
}
},
displayDetails(book) {
elements.detailsContainer.innerHTML = this.generateDetailsHTML(book);
// Add event listeners
document.getElementById('download-button')
.addEventListener('click', () => this.downloadBook(book));
document.getElementById('close-details')
.addEventListener('click', modal.close);
},
generateDetailsHTML(book) {
return `
<div class="uk-card uk-card-default uk-child-width-1-2" uk-grid>
<div class="uk-card-media-left uk-cover-container uk-padding">
<img class="uk-height-medium" src="${book.preview || ''}" alt="Book Preview" uk-cover>
<canvas width="299" height="461"></canvas>
</div>
<div class="uk-card-body">
<h3>${book.title || 'No title available'}</h3>
<p><strong>Author:</strong> ${book.author || 'N/A'}</p>
<p><strong>Publisher:</strong> ${book.publisher || 'N/A'}</p>
<p><strong>Year:</strong> ${book.year || 'N/A'}</p>
<p><strong>Language:</strong> ${book.language || 'N/A'}</p>
<p><strong>Format:</strong> ${book.format || 'N/A'}</p>
<p><strong>Size:</strong> ${book.size || 'N/A'}</p>
</div>
<button id="download-button" class="uk-button uk-button-primary" type="button">Download</button>
<button id="close-details" class="uk-button uk-button-default uk-modal-close" type="button">Close</button>
</div>
<ul uk-accordion>
<li>
<a class="uk-accordion-title" href>Further Information</a>
<div class="uk-accordion-content">
${this.generateInfoList(book.info)}
</div>
</li>
</ul>
`;
},
generateInfoList(info) {
if (!info) return '';
const listItems = Object.entries(info)
.map(([key, values]) => `
<li><strong>${key}:</strong> ${values.join(', ')}</li>
`)
.join('');
return `<ul class="uk-list uk-list-bullet">${listItems}</ul>`;
},
async downloadBook(book) {
if (!book) return;
try {
utils.showLoading(elements.searchLoading);
await utils.fetchJson(
`${API_ENDPOINTS.download}?id=${encodeURIComponent(book.id)}`
);
modal.close();
status.fetch();
} catch (error) {
console.error('Download error:', error);
} finally {
utils.hideLoading(elements.searchLoading);
}
},
handleDetailsError(error) {
console.error('Details error:', error);
elements.detailsContainer.innerHTML = `
<p>Error loading details. Please try again.</p>
<div class="details-actions">
<button id="close-details" onclick="modal.close()">Close</button>
</div>
`;
document.getElementById('close-details')
.addEventListener('click', modal.close);
}
};
// Status Functions
const status = {
async fetch() {
try {
utils.showLoading(elements.statusLoading);
const data = await utils.fetchJson(API_ENDPOINTS.status);
this.display(data);
queue.updateActiveDownloads();
} catch (error) {
this.handleError(error);
} finally {
utils.hideLoading(elements.statusLoading);
}
},
display(data) {
elements.statusTableBody.innerHTML = '';
// Handle each status type
Object.entries(data).forEach(([status, booksInStatus]) => {
// If the status section has books
if (Object.keys(booksInStatus).length > 0) {
// For each book in this status
Object.entries(booksInStatus).forEach(([bookId, bookData]) => {
this.addStatusRow(status, bookData);
});
}
});
},
addStatusRow(status, book) {
if (!book.id || !book.title) return;
const statusCell = utils.createElement('td', {
className: `status-${status.toLowerCase()}`,
textContent: status
});
// Priority cell with editable input for queued items
const priorityCell = utils.createElement('td');
if (status === 'queued') {
const priorityInput = utils.createElement('input', {
type: 'number',
className: 'uk-input uk-form-small uk-form-width-xsmall',
value: book.priority || 0,
min: 0,
onchange: () => queue.setPriority(book.id, parseInt(priorityInput.value))
});
priorityCell.appendChild(priorityInput);
} else {
priorityCell.textContent = book.priority || '-';
}
// Title with download link if available
let titleElement;
if (book.download_path != null) {
titleElement = utils.createElement('a', {
href: `/request/api/localdownload?id=${book.id}`,
target: '_blank',
textContent: book.title || 'N/A',
className: 'uk-link'
});
} else {
titleElement = document.createTextNode(book.title || 'N/A');
}
const titleCell = utils.createElement('td');
titleCell.appendChild(titleElement);
// Progress cell
const progressCell = utils.createElement('td');
if (status === 'downloading' && book.progress !== undefined) {
const progressBar = utils.createElement('progress', {
className: 'uk-progress',
value: book.progress,
max: 100
});
progressCell.appendChild(progressBar);
progressCell.appendChild(document.createTextNode(` ${Math.round(book.progress)}%`));
} else {
progressCell.textContent = '-';
}
// Actions cell
const actionsCell = utils.createElement('td');
if (status === 'queued' || status === 'downloading') {
const cancelBtn = utils.createElement('button', {
className: 'uk-button uk-button-danger uk-button-small',
textContent: 'Cancel',
onclick: () => queue.cancelDownload(book.id)
});
actionsCell.appendChild(cancelBtn);
}
const row = utils.createElement('tr', {
'data-book-id': book.id,
'data-status': status
}, [
statusCell,
priorityCell,
titleCell,
progressCell,
actionsCell
]);
elements.statusTableBody.appendChild(row);
},
createPreviewCell(previewUrl) {
const cell = utils.createElement('td');
if (previewUrl) {
const img = utils.createElement('img', {
src: previewUrl,
alt: 'Book Preview',
style: 'max-width: 60px; height: auto;'
});
cell.appendChild(img);
} 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',
className: 'error-message',
textContent: 'Error loading status. Will retry automatically.'
})
]);
elements.statusTableBody.appendChild(errorRow);
}
};
// Modal Functions
const modal = {
open() {
elements.modalOverlay.classList.add('active');
},
close() {
elements.modalOverlay.classList.remove('active');
modalDetails = null;
}
};
// Theme Management
const theme = {
STORAGE_KEY: 'preferred-theme',
init() {
this.loadTheme();
this.setupListeners();
},
loadTheme() {
const savedTheme = localStorage.getItem(this.STORAGE_KEY) || 'auto';
this.applyThemePreference(savedTheme);
this.updateButtonText(savedTheme);
},
applyThemePreference(preference) {
if (preference === 'auto') {
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', isDarkMode ? 'dark' : 'light');
} else {
document.documentElement.setAttribute('data-theme', preference);
}
},
setupListeners() {
// Listen for system theme changes
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkModeMediaQuery.addEventListener('change', (e) => {
if (localStorage.getItem(this.STORAGE_KEY) === 'auto') {
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
}
});
// Theme selection listeners
const themeLinks = document.querySelectorAll('.uk-dropdown-nav a[data-theme]');
themeLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const selectedTheme = e.target.getAttribute('data-theme');
localStorage.setItem(this.STORAGE_KEY, selectedTheme);
this.applyThemePreference(selectedTheme);
this.updateButtonText(selectedTheme);
});
});
},
updateButtonText(currentTheme) {
const themeText = currentTheme.charAt(0).toUpperCase() + currentTheme.slice(1);
const themeTextElement = document.getElementById('theme-text');
if (themeTextElement) {
themeTextElement.textContent = themeText;
}
}
};
// Debug Functions
const debug = {
init() {
if (!elements.debug.form) return;
elements.debug.form.addEventListener('submit', (e) => {
e.preventDefault(); // Prevent default form submission
if (elements.debug.button.disabled) {
return; // Prevent multiple submissions
}
// Disable button and show spinner
elements.debug.spinner.classList.remove('uk-hidden');
elements.debug.button.disabled = true;
// Use fetch to make the request
fetch(elements.debug.form.action)
.then(response => {
if (!response.ok) {
throw new Error('Debug request failed');
}
// Store content disposition before returning blob
const contentDisposition = response.headers.get('Content-Disposition');
return response.blob().then(blob => {
return { blob, contentDisposition, url: response.url };
});
})
.then(data => {
// Create and click a download link
const url = URL.createObjectURL(data.blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
// Extract filename from Content-Disposition header, URL, or use default
let filename;
if (data.contentDisposition) {
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
const matches = filenameRegex.exec(data.contentDisposition);
if (matches && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
}
}
if (!filename) {
// Extract from URL if header is not available
const urlParts = data.url.split('/');
filename = urlParts[urlParts.length - 1];
}
a.download = filename || "cwa-book-downloader-debug.zip";
document.body.appendChild(a);
a.click();
// Clean up
setTimeout(() => {
URL.revokeObjectURL(url);
document.body.removeChild(a);
// Only reset UI after download starts
elements.debug.spinner.classList.add('uk-hidden');
elements.debug.button.disabled = false;
}, 100);
})
.catch(error => {
console.error('Debug download error:', error);
// Reset UI on error
elements.debug.spinner.classList.add('uk-hidden');
elements.debug.button.disabled = false;
// Show error notification
UIkit.notification({
message: 'Error generating debug file. Please try again.',
status: 'danger',
pos: 'top-center',
timeout: 5000
});
});
});
}
};
// Restart Functions
const restart = {
init() {
if (!elements.restart.form) return;
elements.restart.form.addEventListener('submit', (e) => {
e.preventDefault(); // Prevent default form submission
if (elements.restart.button.disabled) {
return; // Prevent multiple submissions
}
// Show confirmation dialog
UIkit.modal.confirm('Are you sure you want to restart the application? This will interrupt any ongoing downloads.').then(() => {
// User confirmed, proceed with restart
elements.restart.spinner.classList.remove('uk-hidden');
elements.restart.button.disabled = true;
// Show restarting message
UIkit.notification({
message: 'Application restarting...',
status: 'warning',
pos: 'top-center',
timeout: 5000
});
// Make the restart request - this will kill the server
fetch(elements.restart.form.action)
.catch(() => {
// Expected to fail when server shuts down
});
// Wait a bit then refresh the page to reconnect to the restarted server
setTimeout(() => {
window.location.reload();
}, 3000);
}, () => {
// User cancelled, do nothing
});
});
}
};
// Event Listeners
function setupEventListeners() {
// Search events
elements.search.searchButton.addEventListener('click', () => {
const query = search.buildQuery();
if(query) search.performSearch(query);
});
elements.search.advanced.advSearchButton.addEventListener('click', () => {
const query = search.buildQuery();
if(query) search.performSearch(query);
});
elements.search.searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const query = search.buildQuery();
if(query) search.performSearch(query);
}
});
// Modal close on overlay click
elements.modalOverlay.addEventListener('click', (e) => {
if (e.target === elements.modalOverlay) {
modal.close();
}
});
// Keyboard accessibility
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && elements.modalOverlay.classList.contains('active')) {
modal.close();
}
});
// Download selected books
elements.downloadSelectedButton.addEventListener('click', utils.handleDownloadSelected);
// Queue management buttons
const refreshButton = document.getElementById('refresh-status-button');
if (refreshButton) {
refreshButton.addEventListener('click', () => {
status.fetch();
queue.updateActiveDownloads();
});
}
const clearCompletedButton = document.getElementById('clear-completed-button');
if (clearCompletedButton) {
clearCompletedButton.addEventListener('click', () => {
UIkit.modal.confirm('Are you sure you want to clear all completed downloads?').then(() => {
queue.clearCompleted();
}, () => {
// User cancelled
});
});
}
// 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[data-sort]');
headers.forEach((header) => {
let sortOrder = 'asc';
header.addEventListener('click', () => {
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
function init() {
setupEventListeners();
theme.init(); // Initialize theme management
debug.init(); // Initialize debug functionality
restart.init(); // Initialize restart functionality
status.fetch();
queue.updateActiveDownloads();
setInterval(() => status.fetch(), REFRESH_INTERVAL);
}
init();
});