import { type QueueItemDto, listQueueItems, queueItemFileUrl, removeQueueItem, } from '../../api/queueApi'; import queuePanelTemplate from './QueuePanel.html'; import './QueuePanel.scss'; /** Intervalle de sondage de `GET /api/queue`, en l'absence de push temps réel (à venir). */ const POLL_INTERVAL_MS = 1500; /** Délai entre deux déclenchements de téléchargement lors d'un "tout télécharger" (limite les blocages navigateur sur les téléchargements multiples). */ const BULK_DOWNLOAD_STAGGER_MS = 250; const STATUS_LABELS: Record = { pending: 'En attente', running: 'En cours', completed: 'Terminé', failed: 'Échec', cancelled: 'Annulé', }; const MODE_LABELS: Record = { video: 'Vidéo', audio: 'Audio', thumbnail: 'Miniature', }; /** Contrôleur exposé par {@link mountQueuePanel}. */ export interface QueuePanelController { /** Recharge immédiatement la liste, sans attendre le prochain sondage. */ refresh: () => void; } /** * @param item - Élément dont on veut savoir s'il est encore actionnable (en attente/en cours). * @returns `true` si l'élément peut encore être annulé. */ function isActive(item: QueueItemDto): boolean { return item.status === 'pending' || item.status === 'running'; } /** * @param item - Élément à tester. * @returns `true` si un fichier est disponible au téléchargement pour cet élément. */ function isDownloadable(item: QueueItemDto): boolean { return item.status === 'completed' && item.fileName !== null; } /** * Supprime un élément de la file au clic sur son bouton "Annuler"/"Supprimer". * * @param id - Identifiant de l'élément à supprimer. * @param button - Bouton cliqué, désactivé pendant la requête. * @param onDone - Rappelé après la requête (succès ou échec) pour rafraîchir la liste. */ async function handleRemove( id: string, button: HTMLButtonElement, onDone: () => void, ): Promise { button.disabled = true; try { await removeQueueItem(id); } catch (error) { console.error('Échec de la suppression', error); } finally { onDone(); } } /** * Déclenche, avec un léger décalage entre chacun, le téléchargement de tous * les fichiers actuellement disponibles (évite le blocage navigateur des * téléchargements multiples déclenchés au même instant). * * @param button - Bouton "Tout télécharger" cliqué, désactivé pendant l'opération. */ async function handleDownloadAll(button: HTMLButtonElement): Promise { button.disabled = true; try { const items = await listQueueItems(); const downloadable = items.filter(isDownloadable); downloadable.forEach((item, index) => { setTimeout(() => { const link = document.createElement('a'); link.href = queueItemFileUrl(item.id); document.body.append(link); link.click(); link.remove(); }, index * BULK_DOWNLOAD_STAGGER_MS); }); } catch (error) { console.error('Échec du téléchargement groupé', error); } finally { button.disabled = false; } } /** * Construit la ligne d'affichage d'un élément de la file. * * @param item - Élément à afficher. * @param onChanged - Rappelé après une action (annulation/suppression) pour rafraîchir la liste. * @returns L'élément `
  • ` prêt à être inséré dans la liste. */ function renderItem(item: QueueItemDto, onChanged: () => void): HTMLLIElement { const li = document.createElement('li'); li.className = `queue-panel__item queue-panel__item--${item.status}`; const title = document.createElement('p'); title.className = 'queue-panel__item-title'; title.textContent = item.title ?? item.url; li.append(title); const meta = document.createElement('p'); meta.className = 'queue-panel__item-meta'; meta.textContent = `${MODE_LABELS[item.mode.kind]} · ${STATUS_LABELS[item.status]}`; li.append(meta); if (isActive(item)) { const progress = document.createElement('progress'); progress.className = 'queue-panel__item-progress'; progress.max = 100; progress.value = item.progress; li.append(progress); } if (item.status === 'failed' && item.error) { const error = document.createElement('p'); error.className = 'queue-panel__item-error'; error.textContent = item.error; li.append(error); } if (item.status === 'completed' && !item.fileName) { const note = document.createElement('p'); note.className = 'queue-panel__item-meta'; note.textContent = 'Déjà téléchargé'; li.append(note); } const actions = document.createElement('div'); actions.className = 'queue-panel__item-actions'; const removeButton = document.createElement('button'); removeButton.type = 'button'; removeButton.className = 'secondary'; removeButton.textContent = isActive(item) ? 'Annuler' : 'Supprimer'; removeButton.addEventListener('click', () => { handleRemove(item.id, removeButton, onChanged).catch((error: unknown) => { console.error('Erreur inattendue lors de la suppression', error); }); }); if (isDownloadable(item)) { const link = document.createElement('a'); link.href = queueItemFileUrl(item.id); link.textContent = 'Télécharger'; link.className = 'queue-panel__item-download'; actions.append(link); } actions.append(removeButton); li.append(actions); return li; } /** * Rend la liste des éléments dans le panneau et ajuste la visibilité du * bouton "Tout télécharger" selon qu'il existe au moins un fichier prêt. * * @param root - Racine du panneau monté. * @param items - Éléments à afficher. * @param onChanged - Rappelé après une action sur un élément (pour rafraîchir la liste). */ function render(root: HTMLElement, items: readonly QueueItemDto[], onChanged: () => void): void { const list = root.querySelector('[data-role="list"]'); const empty = root.querySelector('[data-role="empty"]'); const downloadAllButton = root.querySelector('[data-role="download-all"]'); if (!list || !empty) { return; } empty.hidden = items.length > 0; list.replaceChildren(...items.map((item) => renderItem(item, onChanged))); if (downloadAllButton) { downloadAllButton.hidden = !items.some(isDownloadable); } } /** * Monte le panneau de file d'attente dans l'élément racine fourni, et * démarre le sondage périodique de `GET /api/queue`. * * @param root - Élément DOM dans lequel injecter le panneau. * @returns Un contrôleur permettant de déclencher un rafraîchissement immédiat. */ export function mountQueuePanel(root: HTMLElement): QueuePanelController { root.innerHTML = queuePanelTemplate; let isRefreshing = false; const refresh = (): void => { if (isRefreshing) { return; } isRefreshing = true; listQueueItems() .then((items) => { render(root, items, refresh); }) .catch((error: unknown) => { console.error('Échec du chargement de la file d’attente', error); }) .finally(() => { isRefreshing = false; }); }; const downloadAllButton = root.querySelector('[data-role="download-all"]'); downloadAllButton?.addEventListener('click', () => { handleDownloadAll(downloadAllButton).catch((error: unknown) => { console.error('Erreur inattendue lors du téléchargement groupé', error); }); }); refresh(); setInterval(refresh, POLL_INTERVAL_MS); return { refresh }; }