819acf04f8
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)
240 lines
11 KiB
JavaScript
240 lines
11 KiB
JavaScript
/**
|
|
* Series search functionality for FS7
|
|
*/
|
|
|
|
// Handle series search
|
|
async function handleSeriesSearch() {
|
|
const searchInput = document.getElementById('seriesSearchInput');
|
|
const resultsContainer = document.getElementById('seriesSearchResults');
|
|
|
|
if (!searchInput || !resultsContainer) return;
|
|
|
|
const query = searchInput.value.trim();
|
|
if (!query) {
|
|
alert('Veuillez entrer un nom de série');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
resultsContainer.innerHTML = '<div class="flex justify-center py-8"><span class="loading loading-spinner loading-md"></span><span class="ml-3 text-base-content/60">Recherche de séries TV en cours...</span></div>';
|
|
|
|
// Search on series providers using the dedicated endpoint
|
|
const response = await fetch(`${API_BASE}/series/search?q=${encodeURIComponent(query)}&lang=vf`);
|
|
const data = await response.json();
|
|
|
|
if (data.results && data.results['fs7'] && data.results['fs7'].length > 0) {
|
|
const series = data.results['fs7'];
|
|
let html = `
|
|
<div class="flex items-center gap-2 mb-4">
|
|
<h3 class="text-lg font-semibold"><i class="fa-solid fa-tv"></i> Résultats pour "${escapeHtml(query)}"</h3>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
`;
|
|
|
|
series.forEach(s => {
|
|
let coverImage = s.cover_image || '';
|
|
|
|
// Convert relative poster.php URLs to absolute URLs
|
|
if (coverImage.startsWith('/poster.php?url=')) {
|
|
const actualUrl = coverImage.replace('/poster.php?url=', '');
|
|
coverImage = actualUrl;
|
|
} else if (coverImage.startsWith('/')) {
|
|
coverImage = 'https://fs7.lol' + coverImage;
|
|
}
|
|
|
|
html += `
|
|
<div class="card bg-base-200 border border-base-300 shadow-sm" id="series-fs7-${encodeURIComponent(s.url)}">
|
|
<div class="card-body p-4">
|
|
<div class="flex justify-between items-start">
|
|
<h4 class="font-semibold text-base">${escapeHtml(s.title)}</h4>
|
|
<span class="badge badge-sm badge-ghost"><i class="fa-solid fa-tv"></i> French Stream</span>
|
|
</div>
|
|
${coverImage ? `
|
|
<div class="flex justify-center my-2">
|
|
<img src="${escapeHtml(coverImage)}" alt="" class="max-w-[200px] rounded-lg" onerror="this.style.display='none'">
|
|
</div>
|
|
` : ''}
|
|
<div class="card-actions justify-end mt-2">
|
|
<button class="btn btn-secondary btn-sm" onclick="window.open('${escapeHtml(s.url)}', '_blank')">
|
|
<i class="fa-solid fa-link"></i> Voir sur FS7
|
|
</button>
|
|
<button class="btn btn-primary btn-sm" onclick="loadSeriesEpisodesDirect('${escapeHtml(s.url)}', '${escapeHtml(s.title)}')">
|
|
<i class="fa-solid fa-arrow-down"></i> Télécharger
|
|
</button>
|
|
</div>
|
|
<div id="episodes-fs7-${encodeURIComponent(s.url)}" class="mt-2"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += '</div>';
|
|
resultsContainer.innerHTML = html;
|
|
} else {
|
|
resultsContainer.innerHTML = `
|
|
<div class="text-center py-16 text-base-content/50">
|
|
<i class="fa-solid fa-xmark text-3xl mb-3 block"></i>
|
|
<p>Aucune série trouvée pour "${escapeHtml(query)}"</p>
|
|
<p class="text-xs mt-2 opacity-70">
|
|
Essayez avec un autre titre ou vérifiez l'orthographe
|
|
</p>
|
|
</div>`;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error searching series:', error);
|
|
resultsContainer.innerHTML = `
|
|
<div class="text-center py-16 text-base-content/50">
|
|
<i class="fa-solid fa-xmark text-3xl mb-3 block"></i>
|
|
<p>Erreur lors de la recherche</p>
|
|
<p class="text-xs mt-2 text-error">${error.message}</p>
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
// 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 = `
|
|
<div class="flex items-center gap-2 py-4">
|
|
<span class="loading loading-spinner loading-sm text-primary"></span>
|
|
<span class="text-base-content/60 text-sm">Chargement des épisodes...</span>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="mt-3 space-y-2">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span class="label-text text-xs text-base-content/60">
|
|
<i class="fas fa-list-ol text-primary"></i> ${totalEps} épisodes disponibles
|
|
</span>
|
|
<button class="btn btn-xs btn-success gap-1"
|
|
onclick="downloadAllSeriesEpisodes(this, '${escapeHtml(url)}', '${escapeHtml(title)}')">
|
|
<i class="fas fa-layer-group"></i> Tout télécharger
|
|
</button>
|
|
</div>
|
|
<div class="max-h-48 overflow-y-auto rounded-lg border border-base-300">
|
|
<ul class="divide-y divide-base-300">
|
|
${data.episodes.map((ep, i) => `
|
|
<li class="flex items-center justify-between px-3 py-2 hover:bg-base-200/50 transition-colors">
|
|
<span class="text-sm font-medium">Épisode ${escapeHtml(ep.episode)}</span>
|
|
<button class="btn btn-xs btn-outline btn-success gap-1"
|
|
hx-post="/api/anime/download?url=${escapeHtml(ep.url)}"
|
|
hx-swap="none"
|
|
hx-on::after-request="if(event.detail.successful){this.innerHTML='<i class=\'fas fa-check\'></i>';this.disabled=true;this.classList.remove('btn-outline','btn-success');this.classList.add('btn-ghost','pointer-events-none');setTimeout(()=>{this.innerHTML='<i class=\'fas fa-download\'></i>';this.disabled=false;this.classList.remove('btn-ghost','pointer-events-none');this.classList.add('btn-outline','btn-success')},4000)}"
|
|
title="Télécharger l'épisode ${escapeHtml(ep.episode)}">
|
|
<i class="fas fa-download"></i>
|
|
</button>
|
|
</li>
|
|
`).join('')}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
`;
|
|
episodesContainer.innerHTML = html;
|
|
} else {
|
|
episodesContainer.innerHTML = `
|
|
<div class="text-center py-4 text-base-content/50 text-sm">
|
|
<i class="fas fa-inbox mb-1 block"></i>
|
|
Aucun épisode disponible
|
|
</div>
|
|
`;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading episodes:', error);
|
|
episodesContainer.innerHTML = `
|
|
<div class="alert alert-error alert-sm text-xs">
|
|
<i class="fas fa-triangle-exclamation"></i>
|
|
<span>Erreur: ${error.message}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 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 = '<span class="loading loading-spinner loading-xs"></span> 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 = '<i class="fas fa-check"></i>';
|
|
btn.disabled = true;
|
|
btn.classList.remove('btn-outline', 'btn-success');
|
|
btn.classList.add('btn-ghost', 'pointer-events-none');
|
|
return r;
|
|
});
|
|
})
|
|
);
|
|
|
|
button.innerHTML = '<i class="fas fa-check"></i> 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) {
|
|
showToast('Veuillez sélectionner un épisode', 'warning');
|
|
return;
|
|
}
|
|
|
|
const episodeUrl = select.value;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/anime/download?url=${encodeURIComponent(episodeUrl)}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.ok) {
|
|
showToast(`Téléchargement démarré pour "${title}"`);
|
|
if (typeof loadDownloads === 'function') {
|
|
loadDownloads();
|
|
}
|
|
} else {
|
|
const error = await response.json();
|
|
const errorMessage = error.detail
|
|
? (typeof error.detail === 'string' ? error.detail : JSON.stringify(error.detail))
|
|
: 'Impossible de démarrer le téléchargement';
|
|
showToast(`Erreur : ${errorMessage}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Download error:', error);
|
|
showToast(`Erreur lors du téléchargement : ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Make functions available globally
|
|
window.handleSeriesSearch = handleSeriesSearch;
|
|
window.loadSeriesEpisodesDirect = loadSeriesEpisodesDirect;
|
|
window.downloadSeriesEpisode = downloadSeriesEpisode;
|
|
window.downloadAllSeriesEpisodes = downloadAllSeriesEpisodes;
|