import { type QueueItemDto, listQueueItems, queueDownloadAllUrl, 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; 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 le téléchargement d'un fichier (ou d'un zip groupé) via un lien * `` temporaire — `target="_blank"` en filet de sécurité : si jamais * rien n'est disponible côté serveur (ex. état changé juste avant le * clic), l'éventuelle réponse JSON d'erreur s'ouvre dans un nouvel onglet * plutôt que de remplacer l'application. * * @param href - URL à déclencher en téléchargement. */ function triggerDownload(href: string): void { const link = document.createElement('a'); link.href = href; link.target = '_blank'; link.rel = 'noopener'; document.body.append(link); link.click(); link.remove(); } /** * 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}`; if (item.thumbnailUrl) { const img = document.createElement('img'); img.src = item.thumbnailUrl; img.alt = ''; img.className = 'queue-panel__item-thumb'; li.append(img); } const content = document.createElement('div'); content.className = 'queue-panel__item-content'; const title = document.createElement('p'); title.className = 'queue-panel__item-title'; title.textContent = item.title ?? item.url; content.append(title); const meta = document.createElement('p'); meta.className = 'queue-panel__item-meta'; meta.textContent = `${MODE_LABELS[item.mode.kind]} · ${STATUS_LABELS[item.status]}`; content.append(meta); if (isActive(item)) { const progress = document.createElement('progress'); progress.className = 'queue-panel__item-progress'; progress.max = 100; progress.value = item.progress; content.append(progress); } if (item.status === 'failed' && item.error) { const error = document.createElement('p'); error.className = 'queue-panel__item-error'; error.textContent = item.error; content.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é'; content.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.target = '_blank'; link.rel = 'noopener'; link.textContent = 'Télécharger'; link.className = 'queue-panel__item-download'; actions.append(link); } actions.append(removeButton); content.append(actions); li.append(content); 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', () => { triggerDownload(queueDownloadAllUrl()); }); refresh(); setInterval(refresh, POLL_INTERVAL_MS); return { refresh }; }