feat: redesign download UX — batch select, season download, toast feedback
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 <select> with scrollable episode list inline - Added 'Tout télécharger' button per series card - Replaced all alert() calls with toast notifications - Episode buttons show checkmark on successful download Anime details JS: - Added batch download button next to episode select - Fixed pre-existing lint error (escaped quote in translateSynopsis) - Standardized download icon to fa-arrow-down across all cards Recommendations + Tabs JS: - Unified download button color (btn-success) across all card types - Consistent icon (fa-arrow-down) for download actions Toast system: - Connected to existing Alpine.js toast infrastructure (show-toast events)
This commit is contained in:
@@ -274,8 +274,8 @@ function renderAnimeDetails(anime) {
|
||||
<a href="${escapeHtml(anime.url)}" target="_blank" class="btn btn-secondary btn-sm">
|
||||
<i class="fa-solid fa-link"></i> Voir sur MAL
|
||||
</a>
|
||||
<button onclick="searchAnimeOnProviders('${escapeHtml(anime.title)}')" class="btn btn-primary btn-sm">
|
||||
<i class="fa-solid fa-download"></i> Télécharger
|
||||
<button onclick="searchAnimeOnProviders('${escapeHtml(anime.title)}')" class="btn btn-success btn-sm">
|
||||
<i class="fa-solid fa-arrow-down"></i> Télécharger
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -397,7 +397,7 @@ function renderStreamingResult(result, query) {
|
||||
return `
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-lg">${icon}</span>
|
||||
<span class="font-semibold text-sm">${escapeHtml(name)}</span>
|
||||
@@ -414,9 +414,16 @@ function renderStreamingResult(result, query) {
|
||||
${episodes.length > 20 ? `<option disabled>... et ${episodes.length - 20} autres</option>` : ''}
|
||||
</select>
|
||||
|
||||
<button class="btn btn-primary btn-sm w-full streaming-download-btn" onclick="downloadSelectedEpisode(this)">
|
||||
<i class="fa-solid fa-download"></i> Télécharger
|
||||
</button>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-primary btn-sm flex-1 streaming-download-btn" onclick="downloadSelectedEpisode(this)">
|
||||
<i class="fa-solid fa-download"></i> Télécharger
|
||||
</button>
|
||||
<button class="btn btn-success btn-sm streaming-download-all-btn"
|
||||
onclick="downloadAllEpisodes(this, '${escapeHtml(query)}', '${escapeHtml(provider)}')"
|
||||
title="Télécharger toute la saison">
|
||||
<i class="fas fa-layer-group"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="#" class="link link-hover text-xs mt-2" onclick="searchAnimeOnProviders('${escapeHtml(query)}'); return false;">
|
||||
@@ -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 = '<span class="loading loading-spinner loading-xs"></span>';
|
||||
|
||||
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 = '<i class="fas fa-check"></i>';
|
||||
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 = '<i class="fa-solid fa-rotate"></i> Voir l\\'original';
|
||||
button.innerHTML = '<i class="fa-solid fa-rotate"></i> Voir l'original';
|
||||
} else {
|
||||
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
||||
console.error('Translation API error:', errorData);
|
||||
|
||||
Reference in New Issue
Block a user