From 819acf04f886b7bc607d1039ad453abed479a964 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 11 Apr 2026 21:08:29 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20redesign=20download=20UX=20=E2=80=94=20?= =?UTF-8?q?batch=20select,=20season=20download,=20toast=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Episode list: - Added 'Saison complète' header button to download all episodes at once - Added multi-select mode with checkboxes for batch episode download - Individual download buttons now show visual feedback (checkmark + reset) - Better grid/list toggle with selection state indicators Search results (anime + series): - Redesigned download dropdown with icons, descriptions, spinner on click - Smooth scale/opacity transitions on dropdown open/close - Consistent btn-success color for all download actions Series search JS: - Replaced basic - +
+ + +
@@ -427,6 +434,45 @@ function renderStreamingResult(result, query) { `; } +// Download all episodes from a streaming result card +async function downloadAllEpisodes(button, query, provider) { + const card = button.closest('.card'); + const select = card.querySelector('.streaming-episode-select'); + const totalEps = select.options.length - 1; // exclude disabled options + const hasMore = select.querySelector('option[disabled]'); + + button.disabled = true; + const originalHtml = button.innerHTML; + button.innerHTML = ''; + + let completed = 0; + const promises = []; + + for (const option of select.options) { + if (!option.value || option.disabled) continue; + promises.push( + fetch(`${API_BASE}/anime/download?url=${encodeURIComponent(option.value)}`, { method: 'POST' }) + .then(r => { completed++; return r; }) + ); + } + + const results = await Promise.allSettled(promises); + const successCount = results.filter(r => r.status === 'fulfilled').length; + + button.innerHTML = ''; + showToast(`${successCount} épisodes mis en file de téléchargement`); + + setTimeout(() => { + button.innerHTML = originalHtml; + button.disabled = false; + }, 4000); + + // Refresh downloads list + if (typeof loadDownloads === 'function') { + loadDownloads(); + } +} + // Download selected episode from streaming results async function downloadSelectedEpisode(button) { const select = button.parentElement.querySelector('.streaming-episode-select'); @@ -519,7 +565,7 @@ async function translateSynopsis(synopsisId, button) { synopsisElement.textContent = data.translatedText; synopsisElement.dataset.translated = 'true'; - button.innerHTML = ' Voir l\\'original'; + button.innerHTML = ' Voir l'original'; } else { const errorData = await response.json().catch(() => ({ detail: 'Unknown error' })); console.error('Translation API error:', errorData); diff --git a/static/js/recommendations.js b/static/js/recommendations.js index f762872..f143eca 100644 --- a/static/js/recommendations.js +++ b/static/js/recommendations.js @@ -190,8 +190,8 @@ function renderRecommendationCard(anime) { - @@ -247,8 +247,8 @@ function renderReleaseCard(anime) { - diff --git a/static/js/series-search.js b/static/js/series-search.js index f95bc65..6250ec3 100644 --- a/static/js/series-search.js +++ b/static/js/series-search.js @@ -59,7 +59,7 @@ async function handleSeriesSearch() { Voir sur FS7
@@ -91,51 +91,119 @@ async function handleSeriesSearch() { } } -// Load series episodes directly without redirecting to search +// Load series episodes directly — shows an inline episode list with download buttons async function loadSeriesEpisodesDirect(url, title) { const episodesContainer = document.getElementById(`episodes-fs7-${encodeURIComponent(url)}`); if (!episodesContainer) return; try { - episodesContainer.innerHTML = '
Chargement des épisodes...
'; + episodesContainer.innerHTML = ` +
+ + Chargement des épisodes... +
+ `; const response = await fetch(`${API_BASE}/anime/episodes?url=${encodeURIComponent(url)}&lang=vf`); const data = await response.json(); if (data.episodes && data.episodes.length > 0) { + const totalEps = data.episodes.length; let html = ` -
- - - +
+
+ + ${totalEps} épisodes disponibles + + +
+
+
    + ${data.episodes.map((ep, i) => ` +
  • + Épisode ${escapeHtml(ep.episode)} + +
  • + `).join('')} +
+
`; episodesContainer.innerHTML = html; } else { - episodesContainer.innerHTML = '
Aucun épisode disponible
'; + episodesContainer.innerHTML = ` +
+ + Aucun épisode disponible +
+ `; } } catch (error) { console.error('Error loading episodes:', error); - episodesContainer.innerHTML = `
Erreur: ${error.message}
`; + episodesContainer.innerHTML = ` +
+ + Erreur: ${error.message} +
+ `; } } -// Download series episode +// Download all series episodes +async function downloadAllSeriesEpisodes(button, url, title) { + const container = button.closest('.mt-3'); + const episodeBtns = container.querySelectorAll('ul button[hx-post*="/api/anime/download"]'); + + // Visual feedback: disable button, show spinner + button.disabled = true; + const originalHtml = button.innerHTML; + button.innerHTML = ' En cours...'; + + let completed = 0; + const total = episodeBtns.length; + + const results = await Promise.allSettled( + [...episodeBtns].map(btn => { + const hxPost = btn.getAttribute('hx-post'); + const epUrl = hxPost.split('url=')[1]; + return fetch(`${API_BASE}/anime/download?url=${encodeURIComponent(epUrl)}`, { method: 'POST' }) + .then(r => { + completed++; + // Visual: mark episode button as done + btn.innerHTML = ''; + btn.disabled = true; + btn.classList.remove('btn-outline', 'btn-success'); + btn.classList.add('btn-ghost', 'pointer-events-none'); + return r; + }); + }) + ); + + button.innerHTML = ' Terminé'; + showToast(`${completed} épisodes de "${title}" mis en file`); + + // Reset button after delay + setTimeout(() => { + button.innerHTML = originalHtml; + button.disabled = false; + }, 5000); +} + +// Download series episode (single - kept for compatibility) async function downloadSeriesEpisode(url, title) { const select = document.getElementById(`select-episodes-${encodeURIComponent(url)}`); if (!select || !select.value) { - alert('Veuillez sélectionner un épisode'); + showToast('Veuillez sélectionner un épisode', 'warning'); return; } @@ -147,8 +215,7 @@ async function downloadSeriesEpisode(url, title) { }); if (response.ok) { - alert(`Téléchargement démarré pour "${title}"`); - // Refresh downloads + showToast(`Téléchargement démarré pour "${title}"`); if (typeof loadDownloads === 'function') { loadDownloads(); } @@ -157,11 +224,11 @@ async function downloadSeriesEpisode(url, title) { const errorMessage = error.detail ? (typeof error.detail === 'string' ? error.detail : JSON.stringify(error.detail)) : 'Impossible de démarrer le téléchargement'; - alert(`Erreur : ${errorMessage}`); + showToast(`Erreur : ${errorMessage}`, 'error'); } } catch (error) { console.error('Download error:', error); - alert(`Erreur lors du téléchargement : ${error.message}`); + showToast(`Erreur lors du téléchargement : ${error.message}`, 'error'); } } @@ -169,3 +236,4 @@ async function downloadSeriesEpisode(url, title) { window.handleSeriesSearch = handleSeriesSearch; window.loadSeriesEpisodesDirect = loadSeriesEpisodesDirect; window.downloadSeriesEpisode = downloadSeriesEpisode; +window.downloadAllSeriesEpisodes = downloadAllSeriesEpisodes; diff --git a/static/js/tabs.js b/static/js/tabs.js index 810185f..cd7c573 100644 --- a/static/js/tabs.js +++ b/static/js/tabs.js @@ -36,8 +36,8 @@ function renderSeriesRecommendationCard(series) { -
@@ -96,8 +96,8 @@ function renderSeriesReleaseCard(series) { - diff --git a/templates/components/anime_search_results.html b/templates/components/anime_search_results.html index 220e388..44de4f9 100644 --- a/templates/components/anime_search_results.html +++ b/templates/components/anime_search_results.html @@ -5,14 +5,14 @@ {% for item in items %} {% set _key = item.title | lower | trim %} {% if _key not in _groups.items %} - {% set _ = _groups.items.update({_key: { + {% set _ = _groups.items.update({ "title": item.title, "cover": item.cover_image or (item.metadata.poster_image if item.metadata else "") or "", "synopsis": (item.metadata.synopsis if item.metadata and item.metadata.synopsis else ""), "rating": item.metadata.rating if item.metadata and item.metadata.rating else "", "genres": item.metadata.genres if item.metadata and item.metadata.genres else [], "providers": [{ "id": item.provider_id or pid, "url": item.url }] - }}) %} + }) %} {% else %} {% set _existing = _groups.items[_key] %} {% if not _existing.cover and item.cover_image %} @@ -35,6 +35,7 @@ {% set first_url = group.providers[0].url %}
+
+ +
+

{{ group.title }}

{% if group.rating %} @@ -63,6 +67,7 @@
{% endif %} +
{% for p in group.providers %} +
+ Regarder - + + - - + + + + + + + + +
@@ -28,18 +63,38 @@
{% for ep in episodes %} -
+
+ +
+ +
+
EP {{ ep.episode_number or loop.index }}
-
+ {% endif %} + + + - @@ -52,23 +107,37 @@
{% for ep in episodes %} -
+
+ +
+ +
+ EP {{ ep.episode_number or loop.index }} - {{ ep.title or 'Épisode ' ~ (ep.episode_number or loop.index) }}
- - @@ -85,3 +154,52 @@ {% endif %}
+ + diff --git a/templates/components/series_search_results.html b/templates/components/series_search_results.html index 43ac5eb..b22f477 100644 --- a/templates/components/series_search_results.html +++ b/templates/components/series_search_results.html @@ -5,12 +5,12 @@ {% for item in items %} {% set _key = item.title | lower | trim %} {% if _key not in _groups.items %} - {% set _ = _groups.items.update({_key: { + {% set _ = _groups.items.update({ "title": item.title, "cover": item.cover_image or "", "synopsis": (item.metadata.synopsis if item.metadata and item.metadata.synopsis else ""), "providers": [{ "id": item.provider_id or pid, "url": item.url }] - }}) %} + }) %} {% else %} {% set _existing = _groups.items[_key] %} {% if not _existing.cover and item.cover_image %} @@ -27,6 +27,7 @@ {% set first_url = group.providers[0].url %}
+
+ +
+

{{ group.title }}

@@ -44,6 +48,7 @@

{{ group.synopsis }}

{% endif %} +
{% for p in group.providers %} +
+ Regarder - + +