fix: resolve all DaisyUI audit issues
- settings.js: replace broken CSS vars with getThemeColor() helper - base.html: add bg-primary text-primary-content active state to drawer - All templates: btn-small -> btn-sm (DaisyUI standard) - Delete orphan templates/components/header.html - auth-utils.js: fix .show class -> use hidden (Tailwind) - login.html: remove redundant auth-* classes, keep DaisyUI only - auth-ui.js: update form selector for cleanup - watchlist.html: fix nav active class styling - 4 JS files (series-search, tabs, recommendations, anime-details): - Replace all old CSS classes with DaisyUI/Tailwind - Remove hardcoded colors, use theme-aware classes - loading-spinner -> DaisyUI loading component - no-results/search-results -> Tailwind utility layout - All badges -> DaisyUI badge variants
This commit is contained in:
File diff suppressed because one or more lines are too long
+95
-86
@@ -7,7 +7,7 @@ async function searchAnimeDetails(query, malId = null) {
|
||||
if (!resultsContainer) return;
|
||||
|
||||
try {
|
||||
resultsContainer.innerHTML = '<div class="loading-spinner">Recherche en cours...</div>';
|
||||
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 en cours...</span></div>';
|
||||
|
||||
// If we have a MAL ID, fetch directly by ID, otherwise search by query
|
||||
let malUrl;
|
||||
@@ -81,10 +81,10 @@ async function searchAnimeDetails(query, malId = null) {
|
||||
// Only add header and wrapper if we have results
|
||||
if (hasResults) {
|
||||
streamingParts.unshift(
|
||||
`<div class="streaming-results-header">
|
||||
<h3><i class="fa-solid fa-film"></i> Résultats de streaming</h3>
|
||||
`<div class="flex items-center gap-2 mb-4 mt-5">
|
||||
<h3 class="text-lg font-semibold"><i class="fa-solid fa-film"></i> Résultats de streaming</h3>
|
||||
</div>
|
||||
<div class="search-results" style="margin-top: 20px;">`
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">`
|
||||
);
|
||||
streamingParts.push('</div>');
|
||||
streamingHtml = streamingParts.join('');
|
||||
@@ -109,9 +109,10 @@ async function searchAnimeDetails(query, malId = null) {
|
||||
// MAL found nothing but we have streaming results
|
||||
if (streamingHtml) {
|
||||
resultsContainer.innerHTML = `
|
||||
<div class="no-results" style="margin-bottom: 20px;">
|
||||
<p><i class="fa-solid fa-circle-info"></i> Aucune fiche trouvée sur MyAnimeList pour "${escapeHtml(query)}"</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; color: #888;">
|
||||
<div class="text-center py-12 text-base-content/50 mb-5">
|
||||
<i class="fa-solid fa-circle-info text-3xl mb-3 block"></i>
|
||||
<p>Aucune fiche trouvée sur MyAnimeList pour "${escapeHtml(query)}"</p>
|
||||
<p class="text-xs mt-2 text-base-content/40">
|
||||
Essayez le nom en anglais ou japonais (ex: "Frieren: Beyond Journey's End")
|
||||
</p>
|
||||
</div>
|
||||
@@ -124,9 +125,10 @@ async function searchAnimeDetails(query, malId = null) {
|
||||
}
|
||||
} else {
|
||||
resultsContainer.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p><i class="fa-solid fa-xmark"></i> Aucun résultat trouvé pour "${escapeHtml(query)}"</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; color: #888;">
|
||||
<div class="text-center py-16 text-base-content/50">
|
||||
<i class="fa-solid fa-xmark text-3xl mb-3 block"></i>
|
||||
<p>Aucun résultat trouvé pour "${escapeHtml(query)}"</p>
|
||||
<p class="text-xs mt-2 text-base-content/40">
|
||||
Essayez le nom en anglais ou japonais (ex: "Frieren: Beyond Journey's End", "One Piece")
|
||||
</p>
|
||||
</div>
|
||||
@@ -137,9 +139,10 @@ async function searchAnimeDetails(query, malId = null) {
|
||||
} catch (error) {
|
||||
console.error('Error searching anime details:', error);
|
||||
resultsContainer.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p><i class="fa-solid fa-xmark"></i> Erreur lors de la recherche.</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; color: #ff6b6b;">${error.message}</p>
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
@@ -176,10 +179,10 @@ async function getProviderSearchResults(query) {
|
||||
// Only add header and wrapper if we have results
|
||||
if (hasResults) {
|
||||
htmlParts.unshift(
|
||||
`<div class="streaming-results-header">
|
||||
<h3><i class="fa-solid fa-film"></i> Résultats de streaming</h3>
|
||||
`<div class="flex items-center gap-2 mb-4 mt-5">
|
||||
<h3 class="text-lg font-semibold"><i class="fa-solid fa-film"></i> Résultats de streaming</h3>
|
||||
</div>
|
||||
<div class="search-results" style="margin-top: 20px;">`
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">`
|
||||
);
|
||||
htmlParts.push('</div>');
|
||||
}
|
||||
@@ -237,24 +240,24 @@ function renderAnimeDetails(anime) {
|
||||
});
|
||||
|
||||
return `
|
||||
<div class="anime-details-card">
|
||||
<div class="card bg-base-200 border border-base-300 shadow-lg">
|
||||
<!-- Header with poster and basic info -->
|
||||
<div class="anime-details-header">
|
||||
${imageUrl ? `<img src="${escapeHtml(imageUrl)}" alt="" class="anime-details-poster">` : ''}
|
||||
<div class="flex flex-col md:flex-row gap-4 p-4">
|
||||
${imageUrl ? `<img src="${escapeHtml(imageUrl)}" alt="" class="w-40 h-56 object-cover rounded-lg shrink-0">` : ''}
|
||||
|
||||
<div class="anime-details-info">
|
||||
<h2 class="anime-details-title">${escapeHtml(anime.title)}</h2>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2 class="text-xl font-bold">${escapeHtml(anime.title)}</h2>
|
||||
${anime.title_english && anime.title_english !== anime.title ? `
|
||||
<p class="anime-details-subtitle">${escapeHtml(anime.title_english)}</p>
|
||||
<p class="text-sm text-base-content/60">${escapeHtml(anime.title_english)}</p>
|
||||
` : ''}
|
||||
|
||||
<div class="anime-details-meta">
|
||||
${score > 0 ? `<div class="anime-details-rating"><i class="fa-solid fa-star"></i> ${score.toFixed(2)}</div>` : ''}
|
||||
${rank > 0 ? `<div class="anime-details-rank">#${rank}</div>` : ''}
|
||||
${popularity > 0 ? `<div class="anime-details-popularity">Popularity #${popularity}</div>` : ''}
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
${score > 0 ? `<span class="badge badge-warning badge-sm"><i class="fa-solid fa-star"></i> ${score.toFixed(2)}</span>` : ''}
|
||||
${rank > 0 ? `<span class="badge badge-outline badge-sm">#${rank}</span>` : ''}
|
||||
${popularity > 0 ? `<span class="badge badge-ghost badge-sm">Popularité #${popularity}</span>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="anime-details-stats">
|
||||
<div class="flex flex-wrap gap-x-4 gap-y-1 mt-3 text-sm text-base-content/70">
|
||||
${anime.episodes ? `<span><i class="fa-solid fa-tv"></i> ${anime.episodes} épisodes</span>` : ''}
|
||||
${anime.status ? `<span><i class="fa-solid fa-tower-broadcast"></i> ${translateStatus(anime.status)}</span>` : ''}
|
||||
${anime.duration ? `<span><i class="fa-solid fa-clock"></i> ${escapeHtml(anime.duration)}</span>` : ''}
|
||||
@@ -262,16 +265,16 @@ function renderAnimeDetails(anime) {
|
||||
</div>
|
||||
|
||||
${studios.length > 0 ? `
|
||||
<div class="anime-details-studios">
|
||||
<div class="text-sm mt-2 text-base-content/60">
|
||||
Studio: ${studios.map(s => escapeHtml(s)).join(', ')}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="anime-details-actions">
|
||||
<a href="${escapeHtml(anime.url)}" target="_blank" class="btn btn-secondary btn-small">
|
||||
<div class="flex flex-wrap gap-2 mt-3">
|
||||
<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-small">
|
||||
<button onclick="searchAnimeOnProviders('${escapeHtml(anime.title)}')" class="btn btn-primary btn-sm">
|
||||
<i class="fa-solid fa-download"></i> Télécharger
|
||||
</button>
|
||||
</div>
|
||||
@@ -280,39 +283,40 @@ function renderAnimeDetails(anime) {
|
||||
|
||||
<!-- Genres and themes -->
|
||||
${(genres.length > 0 || themes.length > 0) ? `
|
||||
<div class="anime-details-tags">
|
||||
${genres.map(g => `<span class="anime-details-tag genre">${escapeHtml(g)}</span>`).join('')}
|
||||
${themes.map(t => `<span class="anime-details-tag theme">${escapeHtml(t)}</span>`).join('')}
|
||||
<div class="px-4 pb-3 flex flex-wrap gap-1">
|
||||
${genres.map(g => `<span class="badge badge-primary badge-outline badge-sm">${escapeHtml(g)}</span>`).join('')}
|
||||
${themes.map(t => `<span class="badge badge-accent badge-outline badge-sm">${escapeHtml(t)}</span>`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Synopsis with translation button -->
|
||||
${synopsis ? `
|
||||
<div class="anime-details-section">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
||||
<h3 style="margin: 0;"><i class="fa-solid fa-book"></i> Synopsis</h3>
|
||||
<button onclick="translateSynopsis('${synopsisId}', this)" class="btn btn-secondary btn-small" style="font-size: 12px;">
|
||||
<div class="px-4 pb-4">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h3 class="font-semibold"><i class="fa-solid fa-book"></i> Synopsis</h3>
|
||||
<button onclick="translateSynopsis('${synopsisId}', this)" class="btn btn-secondary btn-sm btn-xs">
|
||||
<i class="fa-solid fa-globe"></i> Traduire en français
|
||||
</button>
|
||||
</div>
|
||||
<p id="${synopsisId}" class="anime-details-synopsis">${escapeHtml(synopsis)}</p>
|
||||
<p id="${synopsisId}" class="text-sm text-base-content/80 leading-relaxed">${escapeHtml(synopsis)}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Seasons (Sequel/Prequel) -->
|
||||
${seasons.length > 0 ? `
|
||||
<div class="anime-details-section">
|
||||
<h3><i class="fa-solid fa-tv"></i> Saisons</h3>
|
||||
<div class="anime-related-list">
|
||||
<div class="px-4 pb-4">
|
||||
<h3 class="font-semibold mb-2"><i class="fa-solid fa-tv"></i> Saisons</h3>
|
||||
<div class="space-y-3">
|
||||
${seasons.map(season => `
|
||||
<div class="anime-related-group">
|
||||
<div class="anime-related-type">${translateRelationType(season.type)}</div>
|
||||
<div class="anime-related-items">
|
||||
<div>
|
||||
<div class="badge badge-info badge-sm mb-1">${translateRelationType(season.type)}</div>
|
||||
<div class="space-y-1">
|
||||
${season.entries.map(entry => `
|
||||
<div class="anime-related-item" onclick="searchAnimeDetails('${escapeHtml(entry.title)}', ${entry.mal_id})" style="cursor: pointer;">
|
||||
${entry.type ? `<span style="color: #FFBF69; font-size: 11px; margin-right: 8px;">${escapeHtml(entry.type)}</span>` : ''}
|
||||
${escapeHtml(entry.title)}
|
||||
${entry.url ? `<a href="${escapeHtml(entry.url)}" target="_blank" style="margin-left: auto; color: #888; font-size: 18px; text-decoration: none;" title="Voir sur MyAnimeList">↗</a>` : ''}
|
||||
<div class="flex items-center gap-2 p-2 rounded-lg hover:bg-base-300/50 cursor-pointer"
|
||||
onclick="searchAnimeDetails('${escapeHtml(entry.title)}', ${entry.mal_id})">
|
||||
${entry.type ? `<span class="badge badge-warning badge-sm shrink-0">${escapeHtml(entry.type)}</span>` : ''}
|
||||
<span class="text-sm">${escapeHtml(entry.title)}</span>
|
||||
${entry.url ? `<a href="${escapeHtml(entry.url)}" target="_blank" class="ml-auto text-base-content/30 hover:text-base-content text-lg" title="Voir sur MyAnimeList" onclick="event.stopPropagation()">↗</a>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
@@ -332,7 +336,7 @@ async function loadStreamingResults(query) {
|
||||
if (!container) return;
|
||||
|
||||
try {
|
||||
container.innerHTML = '<div class="loading-spinner">Recherche des sources de streaming...</div>';
|
||||
container.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 des sources de streaming...</span></div>';
|
||||
|
||||
// Load providers info
|
||||
const providersData = await getProvidersInfo();
|
||||
@@ -357,8 +361,9 @@ async function loadStreamingResults(query) {
|
||||
|
||||
if (successfulResults.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p><i class="fa-solid fa-triangle-exclamation"></i> Aucun résultat de streaming trouvé pour "${escapeHtml(query)}"</p>
|
||||
<div class="text-center py-16 text-base-content/50">
|
||||
<i class="fa-solid fa-triangle-exclamation text-3xl mb-3 block"></i>
|
||||
<p>Aucun résultat de streaming trouvé pour "${escapeHtml(query)}"</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
@@ -366,10 +371,10 @@ async function loadStreamingResults(query) {
|
||||
|
||||
// Display results
|
||||
container.innerHTML = `
|
||||
<div class="streaming-results-header">
|
||||
<h3><i class="fa-solid fa-film"></i> Disponible sur</h3>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<h3 class="text-lg font-semibold"><i class="fa-solid fa-film"></i> Disponible sur</h3>
|
||||
</div>
|
||||
<div class="streaming-results-grid">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
${successfulResults.map(result => renderStreamingResult(result, query)).join('')}
|
||||
</div>
|
||||
`;
|
||||
@@ -377,8 +382,9 @@ async function loadStreamingResults(query) {
|
||||
} catch (error) {
|
||||
console.error('Error loading streaming results:', error);
|
||||
container.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p><i class="fa-solid fa-xmark"></i> Erreur lors de la recherche des sources de streaming.</p>
|
||||
<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 des sources de streaming.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -389,30 +395,34 @@ function renderStreamingResult(result, query) {
|
||||
const { provider, name, icon, episodes } = result;
|
||||
|
||||
return `
|
||||
<div class="streaming-result-card">
|
||||
<div class="streaming-result-header">
|
||||
<span class="streaming-result-icon">${icon}</span>
|
||||
<span class="streaming-result-name">${escapeHtml(name)}</span>
|
||||
<span class="streaming-result-count">${episodes.length} épisodes</span>
|
||||
<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 gap-2">
|
||||
<span class="text-lg">${icon}</span>
|
||||
<span class="font-semibold text-sm">${escapeHtml(name)}</span>
|
||||
</div>
|
||||
<span class="badge badge-ghost badge-sm">${episodes.length} épisodes</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<select class="select select-bordered select-sm w-full streaming-episode-select" data-provider="${provider}" data-query="${escapeHtml(query)}">
|
||||
<option value="">Sélectionner un épisode</option>
|
||||
${episodes.slice(0, 20).map(ep => `
|
||||
<option value="${escapeHtml(ep.url)}">Épisode ${ep.episode}</option>
|
||||
`).join('')}
|
||||
${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>
|
||||
|
||||
<a href="#" class="link link-hover text-xs mt-2" onclick="searchAnimeOnProviders('${escapeHtml(query)}'); return false;">
|
||||
Voir tous les épisodes sur ${escapeHtml(name)} →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="streaming-result-episodes">
|
||||
<select class="streaming-episode-select" data-provider="${provider}" data-query="${escapeHtml(query)}">
|
||||
<option value="">Sélectionner un épisode</option>
|
||||
${episodes.slice(0, 20).map(ep => `
|
||||
<option value="${escapeHtml(ep.url)}">Épisode ${ep.episode}</option>
|
||||
`).join('')}
|
||||
${episodes.length > 20 ? `<option disabled>... et ${episodes.length - 20} autres</option>` : ''}
|
||||
</select>
|
||||
|
||||
<button class="btn btn-primary btn-small streaming-download-btn" onclick="downloadSelectedEpisode(this)">
|
||||
<i class="fa-solid fa-download"></i> Télécharger
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<a href="#" class="streaming-result-link" onclick="searchAnimeOnProviders('${escapeHtml(query)}'); return false;">
|
||||
Voir tous les épisodes sur ${escapeHtml(name)} →
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -509,7 +519,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);
|
||||
@@ -519,12 +529,12 @@ async function translateSynopsis(synopsisId, button) {
|
||||
console.error('Translation error:', error);
|
||||
synopsisElement.style.opacity = '1';
|
||||
|
||||
// Show user-friendly error
|
||||
// Show user-friendly error using DaisyUI alert styling
|
||||
const errorMessage = document.createElement('div');
|
||||
errorMessage.style.cssText = 'margin-top: 10px; padding: 10px; background: rgba(255, 107, 107, 0.2); border-radius: 8px; font-size: 12px; color: #ff6b6b;';
|
||||
errorMessage.className = 'alert alert-error alert-sm mt-2 text-xs translation-error';
|
||||
errorMessage.innerHTML = `
|
||||
<i class="fa-solid fa-triangle-exclamation"></i> Service de traduction temporairement indisponible.<br>
|
||||
<small>Essayez à nouveau dans quelques instants.</small>
|
||||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||
<span>Service de traduction temporairement indisponible. Essayez à nouveau dans quelques instants.</span>
|
||||
`;
|
||||
|
||||
// Remove existing error message if any
|
||||
@@ -533,7 +543,6 @@ async function translateSynopsis(synopsisId, button) {
|
||||
existingError.remove();
|
||||
}
|
||||
|
||||
errorMessage.className = 'translation-error';
|
||||
synopsisElement.parentElement.appendChild(errorMessage);
|
||||
|
||||
// Auto-remove error after 5 seconds
|
||||
|
||||
@@ -102,7 +102,7 @@ function resetLoading(buttonId, originalText) {
|
||||
|
||||
function switchTab(tab) {
|
||||
const tabs = document.querySelectorAll('.auth-tab');
|
||||
const forms = document.querySelectorAll('.auth-form');
|
||||
const forms = document.querySelectorAll('#loginForm, #registerForm');
|
||||
|
||||
// Remove active states — DaisyUI uses tab-active on tabs, hidden on forms
|
||||
tabs.forEach(t => t.classList.remove('tab-active'));
|
||||
|
||||
@@ -67,12 +67,12 @@ function displayError(elementId, error, defaultMessage = 'Une erreur est survenu
|
||||
}
|
||||
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.classList.add('show');
|
||||
|
||||
errorDiv.classList.remove('hidden');
|
||||
|
||||
// Hide success message if visible
|
||||
const successDiv = document.getElementById(elementId.replace('Error', 'Success'));
|
||||
if (successDiv) {
|
||||
successDiv.classList.remove('show');
|
||||
successDiv.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,12 +89,12 @@ function displaySuccess(elementId, message) {
|
||||
}
|
||||
|
||||
successDiv.textContent = message;
|
||||
successDiv.classList.add('show');
|
||||
|
||||
successDiv.classList.remove('hidden');
|
||||
|
||||
// Hide error message if visible
|
||||
const errorDiv = document.getElementById(elementId.replace('Success', 'Error'));
|
||||
if (errorDiv) {
|
||||
errorDiv.classList.remove('show');
|
||||
errorDiv.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ async function loadRecommendations() {
|
||||
if (!container) return;
|
||||
|
||||
try {
|
||||
container.innerHTML = '<div class="loading-spinner">Analyse de vos téléchargements...</div>';
|
||||
container.innerHTML = '<div class="flex justify-center py-8"><span class="loading loading-spinner loading-md"></span><span class="ml-3 text-base-content/60">Analyse de vos téléchargements...</span></div>';
|
||||
|
||||
const response = await fetch(`${API_BASE}/recommendations?limit=12`);
|
||||
const data = await response.json();
|
||||
@@ -16,17 +16,18 @@ async function loadRecommendations() {
|
||||
console.log('Recommendations response:', data);
|
||||
|
||||
if (data.recommendations && data.recommendations.length > 0) {
|
||||
container.innerHTML = `<div class="recommendations-carousel">${data.recommendations.map(anime =>
|
||||
container.innerHTML = `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">${data.recommendations.map(anime =>
|
||||
renderRecommendationCard(anime)
|
||||
).join('')}</div>`;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p><i class="fa-solid fa-triangle-exclamation"></i> Aucune recommandation disponible pour le moment.</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; color: #888;">
|
||||
<div class="text-center py-16 text-base-content/50">
|
||||
<i class="fa-solid fa-triangle-exclamation text-3xl mb-3 block"></i>
|
||||
<p>Aucune recommandation disponible pour le moment.</p>
|
||||
<p class="text-xs mt-2 text-base-content/40">
|
||||
Soit l'API MyAnimeList est inaccessible, soit vous n'avez pas encore de téléchargements.
|
||||
</p>
|
||||
<button class="btn btn-secondary btn-small" onclick="loadRecommendations()" style="margin-top: 10px;">
|
||||
<button class="btn btn-secondary btn-sm mt-3" onclick="loadRecommendations()">
|
||||
<i class="fa-solid fa-rotate"></i> Réessayer
|
||||
</button>
|
||||
</div>
|
||||
@@ -37,10 +38,11 @@ async function loadRecommendations() {
|
||||
} catch (error) {
|
||||
console.error('Error loading recommendations:', error);
|
||||
container.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p><i class="fa-solid fa-xmark"></i> Erreur lors du chargement des recommandations.</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; color: #ff6b6b;">${error.message}</p>
|
||||
<button class="btn btn-secondary btn-small" onclick="loadRecommendations()" style="margin-top: 10px;">
|
||||
<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 du chargement des recommandations.</p>
|
||||
<p class="text-xs mt-2 text-error">${error.message}</p>
|
||||
<button class="btn btn-secondary btn-sm mt-3" onclick="loadRecommendations()">
|
||||
<i class="fa-solid fa-rotate"></i> Réessayer
|
||||
</button>
|
||||
</div>
|
||||
@@ -57,7 +59,7 @@ async function loadLatestReleases() {
|
||||
if (!container) return;
|
||||
|
||||
try {
|
||||
container.innerHTML = '<div class="loading-spinner">Chargement des dernières sorties...</div>';
|
||||
container.innerHTML = '<div class="flex justify-center py-8"><span class="loading loading-spinner loading-md"></span><span class="ml-3 text-base-content/60">Chargement des dernières sorties...</span></div>';
|
||||
|
||||
const response = await fetch(`${API_BASE}/releases/latest?limit=12`);
|
||||
const data = await response.json();
|
||||
@@ -65,17 +67,18 @@ async function loadLatestReleases() {
|
||||
console.log('Releases response:', data);
|
||||
|
||||
if (data.releases && data.releases.length > 0) {
|
||||
container.innerHTML = `<div class="releases-carousel">${data.releases.map(anime =>
|
||||
container.innerHTML = `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">${data.releases.map(anime =>
|
||||
renderReleaseCard(anime)
|
||||
).join('')}</div>`;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p><i class="fa-solid fa-triangle-exclamation"></i> Aucune sortie disponible pour le moment.</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; color: #888;">
|
||||
<div class="text-center py-16 text-base-content/50">
|
||||
<i class="fa-solid fa-triangle-exclamation text-3xl mb-3 block"></i>
|
||||
<p>Aucune sortie disponible pour le moment.</p>
|
||||
<p class="text-xs mt-2 text-base-content/40">
|
||||
L'API MyAnimeList pourrait être temporairement inaccessible.
|
||||
</p>
|
||||
<button class="btn btn-secondary btn-small" onclick="loadLatestReleases()" style="margin-top: 10px;">
|
||||
<button class="btn btn-secondary btn-sm mt-3" onclick="loadLatestReleases()">
|
||||
<i class="fa-solid fa-rotate"></i> Réessayer
|
||||
</button>
|
||||
</div>
|
||||
@@ -86,10 +89,11 @@ async function loadLatestReleases() {
|
||||
} catch (error) {
|
||||
console.error('Error loading releases:', error);
|
||||
container.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p><i class="fa-solid fa-xmark"></i> Erreur lors du chargement des sorties.</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; color: #ff6b6b;">${error.message}</p>
|
||||
<button class="btn btn-secondary btn-small" onclick="loadLatestReleases()" style="margin-top: 10px;">
|
||||
<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 du chargement des sorties.</p>
|
||||
<p class="text-xs mt-2 text-error">${error.message}</p>
|
||||
<button class="btn btn-secondary btn-sm mt-3" onclick="loadLatestReleases()">
|
||||
<i class="fa-solid fa-rotate"></i> Réessayer
|
||||
</button>
|
||||
</div>
|
||||
@@ -148,44 +152,48 @@ function renderRecommendationCard(anime) {
|
||||
const reason = anime.recommendation_reason || 'Recommandé';
|
||||
|
||||
return `
|
||||
<div class="anime-card-horizontal recommendation-card">
|
||||
${reason ? `<div class="recommendation-badge"><i class="fa-solid fa-lightbulb"></i> ${escapeHtml(reason)}</div>` : ''}
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm relative">
|
||||
${reason ? `<div class="badge badge-accent badge-sm absolute top-2 left-2 z-10"><i class="fa-solid fa-lightbulb"></i> ${escapeHtml(reason)}</div>` : ''}
|
||||
|
||||
<div class="anime-card-header">
|
||||
<div class="anime-card-title">${escapeHtml(anime.title)}</div>
|
||||
${score > 0 ? `<div class="anime-card-rating"><i class="fa-solid fa-star"></i> ${score.toFixed(1)}</div>` : ''}
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<h4 class="card-title text-base">${escapeHtml(anime.title)}</h4>
|
||||
${score > 0 ? `<span class="badge badge-warning badge-sm shrink-0 ml-2"><i class="fa-solid fa-star"></i> ${score.toFixed(1)}</span>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="anime-card-content">
|
||||
${imageUrl ? `<img src="${escapeHtml(imageUrl)}" alt="" class="anime-card-image" onerror="this.style.display='none'">` : ''}
|
||||
<div class="flex gap-3 mt-1">
|
||||
${imageUrl ? `<img src="${escapeHtml(imageUrl)}" alt="" class="w-20 h-28 object-cover rounded-lg shrink-0" onerror="this.style.display='none'">` : ''}
|
||||
|
||||
<div class="anime-card-info">
|
||||
<div class="anime-genres">
|
||||
${genres.slice(0, 3).map(g => `<span class="anime-genre-tag">${escapeHtml(g)}</span>`).join('')}
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 text-sm">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
${genres.slice(0, 3).map(g => `<span class="badge badge-outline badge-sm">${escapeHtml(g)}</span>`).join('')}
|
||||
</div>
|
||||
|
||||
<div class="anime-card-meta">
|
||||
${anime.episodes ? `<i class="fa-solid fa-tv"></i> ${anime.episodes} ep` : ''}
|
||||
${anime.episodes && anime.status ? ' • ' : ''}
|
||||
${anime.status ? translateStatus(anime.status) : ''}
|
||||
<div class="text-base-content/60 text-xs">
|
||||
${anime.episodes ? `<i class="fa-solid fa-tv"></i> ${anime.episodes} ep` : ''}
|
||||
${anime.episodes && anime.status ? ' • ' : ''}
|
||||
${anime.status ? translateStatus(anime.status) : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${anime.synopsis ? `
|
||||
<details class="anime-synopsis">
|
||||
<summary><i class="fa-solid fa-book"></i> Synopsis</summary>
|
||||
<p>${escapeHtml(anime.synopsis.substring(0, 150))}${anime.synopsis.length > 150 ? '...' : ''}</p>
|
||||
</details>
|
||||
` : ''}
|
||||
${anime.synopsis ? `
|
||||
<details class="collapse collapse-arrow border border-base-300 mt-2 bg-base-300/30">
|
||||
<summary class="collapse-title text-xs font-medium py-2 min-h-0"><i class="fa-solid fa-book"></i> Synopsis</summary>
|
||||
<div class="collapse-content text-xs text-base-content/70">
|
||||
<p>${escapeHtml(anime.synopsis.substring(0, 150))}${anime.synopsis.length > 150 ? '...' : ''}</p>
|
||||
</div>
|
||||
</details>
|
||||
` : ''}
|
||||
|
||||
<div class="anime-card-actions">
|
||||
<button class="btn btn-secondary btn-small" onclick="window.open('${escapeHtml(anime.url)}', '_blank')">
|
||||
<i class="fa-solid fa-link"></i> MAL
|
||||
</button>
|
||||
<button class="btn btn-primary btn-small" onclick="searchAnimeOnProviders('${escapeHtml(anime.title)}')">
|
||||
<i class="fa-solid fa-download"></i> Télécharger
|
||||
</button>
|
||||
<div class="card-actions justify-end mt-2">
|
||||
<button class="btn btn-secondary btn-sm" onclick="window.open('${escapeHtml(anime.url)}', '_blank')">
|
||||
<i class="fa-solid fa-link"></i> MAL
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="searchAnimeOnProviders('${escapeHtml(anime.title)}')">
|
||||
<i class="fa-solid fa-download"></i> Télécharger
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -201,44 +209,48 @@ function renderReleaseCard(anime) {
|
||||
const releaseType = anime.release_type || 'Nouveau';
|
||||
|
||||
return `
|
||||
<div class="anime-card-horizontal release-card">
|
||||
<div class="release-badge"><i class="fa-solid fa-fire"></i> ${escapeHtml(releaseType)}</div>
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm relative">
|
||||
<div class="badge badge-error badge-sm absolute top-2 left-2 z-10"><i class="fa-solid fa-fire"></i> ${escapeHtml(releaseType)}</div>
|
||||
|
||||
<div class="anime-card-header">
|
||||
<div class="anime-card-title">${escapeHtml(anime.title)}</div>
|
||||
${score > 0 ? `<div class="anime-card-rating"><i class="fa-solid fa-star"></i> ${score.toFixed(1)}</div>` : ''}
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<h4 class="card-title text-base">${escapeHtml(anime.title)}</h4>
|
||||
${score > 0 ? `<span class="badge badge-warning badge-sm shrink-0 ml-2"><i class="fa-solid fa-star"></i> ${score.toFixed(1)}</span>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="anime-card-content">
|
||||
${imageUrl ? `<img src="${escapeHtml(imageUrl)}" alt="" class="anime-card-image" onerror="this.style.display='none'">` : ''}
|
||||
<div class="flex gap-3 mt-1">
|
||||
${imageUrl ? `<img src="${escapeHtml(imageUrl)}" alt="" class="w-20 h-28 object-cover rounded-lg shrink-0" onerror="this.style.display='none'">` : ''}
|
||||
|
||||
<div class="anime-card-info">
|
||||
<div class="anime-genres">
|
||||
${genres.slice(0, 3).map(g => `<span class="anime-genre-tag" style="color: #ff6b6b; background: rgba(255,107,107,0.15);">${escapeHtml(g)}</span>`).join('')}
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 text-sm">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
${genres.slice(0, 3).map(g => `<span class="badge badge-error badge-outline badge-sm">${escapeHtml(g)}</span>`).join('')}
|
||||
</div>
|
||||
|
||||
<div class="anime-card-meta">
|
||||
${anime.episodes ? `<i class="fa-solid fa-tv"></i> ${anime.episodes} ep` : ''}
|
||||
${anime.episodes && anime.status ? ' • ' : ''}
|
||||
${anime.status ? translateStatus(anime.status) : ''}
|
||||
<div class="text-base-content/60 text-xs">
|
||||
${anime.episodes ? `<i class="fa-solid fa-tv"></i> ${anime.episodes} ep` : ''}
|
||||
${anime.episodes && anime.status ? ' • ' : ''}
|
||||
${anime.status ? translateStatus(anime.status) : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${anime.synopsis ? `
|
||||
<details class="anime-synopsis">
|
||||
<summary><i class="fa-solid fa-book"></i> Synopsis</summary>
|
||||
<p>${escapeHtml(anime.synopsis.substring(0, 150))}${anime.synopsis.length > 150 ? '...' : ''}</p>
|
||||
</details>
|
||||
` : ''}
|
||||
${anime.synopsis ? `
|
||||
<details class="collapse collapse-arrow border border-base-300 mt-2 bg-base-300/30">
|
||||
<summary class="collapse-title text-xs font-medium py-2 min-h-0"><i class="fa-solid fa-book"></i> Synopsis</summary>
|
||||
<div class="collapse-content text-xs text-base-content/70">
|
||||
<p>${escapeHtml(anime.synopsis.substring(0, 150))}${anime.synopsis.length > 150 ? '...' : ''}</p>
|
||||
</div>
|
||||
</details>
|
||||
` : ''}
|
||||
|
||||
<div class="anime-card-actions">
|
||||
<button class="btn btn-secondary btn-small" onclick="window.open('${escapeHtml(anime.url)}', '_blank')">
|
||||
<i class="fa-solid fa-link"></i> MAL
|
||||
</button>
|
||||
<button class="btn btn-primary btn-small" onclick="searchAnimeOnProviders('${escapeHtml(anime.title)}')">
|
||||
<i class="fa-solid fa-download"></i> Télécharger
|
||||
</button>
|
||||
<div class="card-actions justify-end mt-2">
|
||||
<button class="btn btn-secondary btn-sm" onclick="window.open('${escapeHtml(anime.url)}', '_blank')">
|
||||
<i class="fa-solid fa-link"></i> MAL
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="searchAnimeOnProviders('${escapeHtml(anime.title)}')">
|
||||
<i class="fa-solid fa-download"></i> Télécharger
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -246,11 +258,11 @@ function renderReleaseCard(anime) {
|
||||
|
||||
// Get rating color based on score
|
||||
function getRatingColor(score) {
|
||||
if (score >= 9) return '#ffd700';
|
||||
if (score >= 8) return '#2d936c';
|
||||
if (score >= 7) return '#FF9F1C';
|
||||
if (score >= 6) return '#f4a261';
|
||||
return '#888888';
|
||||
if (score >= 9) return 'text-warning';
|
||||
if (score >= 8) return 'text-success';
|
||||
if (score >= 7) return 'text-warning';
|
||||
if (score >= 6) return 'text-warning';
|
||||
return 'text-base-content/40';
|
||||
}
|
||||
|
||||
// Search anime on providers (redirects to anime tab)
|
||||
|
||||
+39
-37
@@ -16,7 +16,7 @@ async function handleSeriesSearch() {
|
||||
}
|
||||
|
||||
try {
|
||||
resultsContainer.innerHTML = '<div class="loading-spinner">Recherche de séries TV en cours...</div>';
|
||||
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`);
|
||||
@@ -25,10 +25,10 @@ async function handleSeriesSearch() {
|
||||
if (data.results && data.results['fs7'] && data.results['fs7'].length > 0) {
|
||||
const series = data.results['fs7'];
|
||||
let html = `
|
||||
<div class="streaming-results-header">
|
||||
<h3><i class="fa-solid fa-tv"></i> Résultats pour "${escapeHtml(query)}"</h3>
|
||||
<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="search-results" style="margin-top: 20px;">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
`;
|
||||
|
||||
series.forEach(s => {
|
||||
@@ -43,25 +43,27 @@ async function handleSeriesSearch() {
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="anime-card" id="series-fs7-${encodeURIComponent(s.url)}">
|
||||
<div class="anime-card-header">
|
||||
<div class="anime-card-title">${escapeHtml(s.title)}</div>
|
||||
<div class="anime-card-provider"><i class="fa-solid fa-tv"></i> French Stream</div>
|
||||
</div>
|
||||
${coverImage ? `
|
||||
<div style="text-align: center; margin: 10px 0;">
|
||||
<img src="${escapeHtml(coverImage)}" alt="" style="max-width: 200px; border-radius: 4px;" onerror="this.style.display='none'">
|
||||
<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>
|
||||
` : ''}
|
||||
<div class="anime-card-actions">
|
||||
<button class="btn btn-secondary btn-small" onclick="window.open('${escapeHtml(s.url)}', '_blank')">
|
||||
<i class="fa-solid fa-link"></i> Voir sur FS7
|
||||
</button>
|
||||
<button class="btn btn-primary btn-small" onclick="loadSeriesEpisodesDirect('${escapeHtml(s.url)}', '${escapeHtml(s.title)}')">
|
||||
<i class="fa-solid fa-download"></i> Voir les épisodes
|
||||
</button>
|
||||
${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-download"></i> Voir les épisodes
|
||||
</button>
|
||||
</div>
|
||||
<div id="episodes-fs7-${encodeURIComponent(s.url)}" class="mt-2"></div>
|
||||
</div>
|
||||
<div id="episodes-fs7-${encodeURIComponent(s.url)}" style="margin-top: 10px;"></div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
@@ -70,9 +72,10 @@ async function handleSeriesSearch() {
|
||||
resultsContainer.innerHTML = html;
|
||||
} else {
|
||||
resultsContainer.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p><i class="fa-solid fa-xmark"></i> Aucune série trouvée pour "${escapeHtml(query)}"</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; opacity: 0.7;">
|
||||
<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>`;
|
||||
@@ -80,9 +83,10 @@ async function handleSeriesSearch() {
|
||||
} catch (error) {
|
||||
console.error('Error searching series:', error);
|
||||
resultsContainer.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p><i class="fa-solid fa-xmark"></i> Erreur lors de la recherche</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; color: #ff6b6b;">${error.message}</p>
|
||||
<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>`;
|
||||
}
|
||||
}
|
||||
@@ -94,38 +98,36 @@ async function loadSeriesEpisodesDirect(url, title) {
|
||||
if (!episodesContainer) return;
|
||||
|
||||
try {
|
||||
episodesContainer.innerHTML = '<div class="loading-spinner">Chargement des épisodes...</div>';
|
||||
episodesContainer.innerHTML = '<div class="flex justify-center py-4"><span class="loading loading-spinner loading-sm"></span><span class="ml-2 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) {
|
||||
let html = `
|
||||
<div style="margin-top: 15px;">
|
||||
<label style="font-size: 12px; color: #FF9F1C; margin-bottom: 5px; display: block;">
|
||||
<div class="mt-3">
|
||||
<label class="label-text text-xs mb-1 block text-warning">
|
||||
<i class="fa-solid fa-tv"></i> Sélectionner un épisode:
|
||||
</label>
|
||||
<select id="select-episodes-${encodeURIComponent(url)}" style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #2a2d32; background: #202327; color: #F2F2F2;">
|
||||
<select id="select-episodes-${encodeURIComponent(url)}" class="select select-bordered select-sm w-full">
|
||||
<option value="">Sélectionner un épisode</option>
|
||||
${data.episodes.map(ep => `
|
||||
<option value="${escapeHtml(ep.url)}">Épisode ${escapeHtml(ep.episode)}</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
<button class="btn btn-primary" style="margin-top: 10px; width: 100%;" onclick="downloadSeriesEpisode('${escapeHtml(url)}', '${escapeHtml(title)}')">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:14px;height:14px;margin-right:4px;">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
<button class="btn btn-primary btn-sm w-full mt-2" onclick="downloadSeriesEpisode('${escapeHtml(url)}', '${escapeHtml(title)}')">
|
||||
<i class="fa-solid fa-download"></i>
|
||||
Télécharger l'épisode
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
episodesContainer.innerHTML = html;
|
||||
} else {
|
||||
episodesContainer.innerHTML = '<div class="no-results" style="margin-top: 10px;">Aucun épisode disponible</div>';
|
||||
episodesContainer.innerHTML = '<div class="text-center py-4 text-base-content/50 text-sm">Aucun épisode disponible</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading episodes:', error);
|
||||
episodesContainer.innerHTML = `<div class="no-results" style="margin-top: 10px; color: #ff6b6b;">Erreur: ${error.message}</div>`;
|
||||
episodesContainer.innerHTML = `<div class="text-center py-4 text-sm text-error">Erreur: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+38
-12
@@ -4,6 +4,16 @@
|
||||
* the settings section is dynamically loaded via HTMX.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Read a DaisyUI theme color from computed CSS custom properties.
|
||||
* Falls back to sensible defaults if the theme variable is not found.
|
||||
*/
|
||||
function getThemeColor(varName, fallback) {
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
const value = style.getPropertyValue(varName).trim();
|
||||
return value || fallback;
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
const data = {
|
||||
default_lang: document.getElementById('default_lang')?.value,
|
||||
@@ -108,8 +118,14 @@ async function loadAutoWeights() {
|
||||
const sc = data.series_count;
|
||||
const total = data.total || 0;
|
||||
|
||||
const primary = getThemeColor('--color-primary', '#6366f1');
|
||||
const secondary = getThemeColor('--color-secondary', '#a3a3a3');
|
||||
const accent = getThemeColor('--color-accent', '#38bdf8');
|
||||
const error = getThemeColor('--color-error', '#f43f5e');
|
||||
const muted = getThemeColor('--color-base-content', '#999');
|
||||
|
||||
if (total === 0) {
|
||||
details.innerHTML = '<span style="color: var(--text-dim);">Aucun telechargement detecte. Ratio par defaut : ' + aw + ' anime / ' + sw + ' serie.</span>';
|
||||
details.innerHTML = `<span style="color: ${muted}; opacity: 0.6;">Aucun telechargement detecte. Ratio par defaut : ${aw} anime / ${sw} serie.</span>`;
|
||||
} else {
|
||||
const pctA = total > 0 ? Math.round(ac / total * 100) : 50;
|
||||
const pctS = total > 0 ? Math.round(sc / total * 100) : 50;
|
||||
@@ -117,17 +133,20 @@ async function loadAutoWeights() {
|
||||
<div style="margin-bottom: 8px;">
|
||||
<strong>${ac}</strong> anime${ac > 1 ? 's' : ''} (${pctA}%) — <strong>${sc}</strong> serie${sc > 1 ? 's' : ''} (${pctS}%)
|
||||
</div>
|
||||
<div style="height: 8px; background: var(--secondary); border-radius: 4px; overflow: hidden; display: flex;">
|
||||
<div style="width: ${pctA}%; background: var(--primary);"></div>
|
||||
<div style="width: ${pctS}%; background: #6CB4EE;"></div>
|
||||
<div class="progress w-full" style="height: 8px;">
|
||||
<div class="progress-bar" style="width: 100%; display: flex; border-radius: 4px; overflow: hidden;">
|
||||
<div style="width: ${pctA}%; background: ${primary};"></div>
|
||||
<div style="width: ${pctS}%; background: ${accent};"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 8px; font-size: 12px;">
|
||||
Ratio applique : <strong style="color: var(--primary);">${aw}</strong> anime / <strong style="color: #6CB4EE;">${sw}</strong> serie
|
||||
Ratio applique : <strong style="color: ${primary};">${aw}</strong> anime / <strong style="color: ${accent};">${sw}</strong> serie
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (e) {
|
||||
details.innerHTML = '<span style="color: var(--danger);">Erreur de chargement</span>';
|
||||
const error = getThemeColor('--color-error', '#f43f5e');
|
||||
details.innerHTML = `<span style="color: ${error};">Erreur de chargement</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,8 +160,13 @@ function updateWeightPreview() {
|
||||
const sw = parseInt(swEl.value) || 0;
|
||||
const total = aw + sw;
|
||||
|
||||
const primary = getThemeColor('--color-primary', '#6366f1');
|
||||
const secondary = getThemeColor('--color-secondary', '#a3a3a3');
|
||||
const accent = getThemeColor('--color-accent', '#38bdf8');
|
||||
const error = getThemeColor('--color-error', '#f43f5e');
|
||||
|
||||
if (total === 0) {
|
||||
preview.innerHTML = '<span style="color: var(--danger);">Les deux poids ne peuvent pas etre a 0</span>';
|
||||
preview.innerHTML = `<span style="color: ${error};">Les deux poids ne peuvent pas etre a 0</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -151,12 +175,14 @@ function updateWeightPreview() {
|
||||
|
||||
preview.innerHTML = `
|
||||
<div style="margin-bottom: 6px;">
|
||||
<span style="color: var(--primary); font-weight: 700;">${pctA}%</span> animes /
|
||||
<span style="color: #6CB4EE; font-weight: 700;">${pctS}%</span> series
|
||||
<span style="color: ${primary}; font-weight: 700;">${pctA}%</span> animes /
|
||||
<span style="color: ${accent}; font-weight: 700;">${pctS}%</span> series
|
||||
</div>
|
||||
<div style="height: 8px; background: var(--secondary); border-radius: 4px; overflow: hidden; display: flex;">
|
||||
<div style="width: ${pctA}%; background: var(--primary); transition: width 0.2s;"></div>
|
||||
<div style="width: ${pctS}%; background: #6CB4EE; transition: width 0.2s;"></div>
|
||||
<div class="progress w-full" style="height: 8px;">
|
||||
<div class="progress-bar" style="width: 100%; display: flex; border-radius: 4px; overflow: hidden;">
|
||||
<div style="width: ${pctA}%; background: ${primary}; transition: width 0.2s;"></div>
|
||||
<div style="width: ${pctS}%; background: ${accent}; transition: width 0.2s;"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
+87
-87
@@ -18,30 +18,28 @@ function renderSeriesRecommendationCard(series) {
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="anime-card-horizontal recommendation-card">
|
||||
<div class="recommendation-badge"><i class="fa-solid fa-music"></i> Série TV populaire</div>
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm">
|
||||
<div class="badge badge-primary badge-sm absolute top-2 right-2 z-10"><i class="fa-solid fa-music"></i> Série TV populaire</div>
|
||||
|
||||
<div class="anime-card-header">
|
||||
<div class="anime-card-title">${escapeHtml(series.title)}</div>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<h4 class="card-title text-base">${escapeHtml(series.title)}</h4>
|
||||
|
||||
<div class="anime-card-content">
|
||||
${coverImage ? `<img src="${escapeHtml(coverImage)}" alt="" class="anime-card-image" onerror="this.style.display='none'">` : ''}
|
||||
<div class="flex gap-3 mt-1">
|
||||
${coverImage ? `<img src="${escapeHtml(coverImage)}" alt="" class="w-20 h-28 object-cover rounded-lg" onerror="this.style.display='none'">` : ''}
|
||||
|
||||
<div class="anime-card-info">
|
||||
<div class="anime-card-meta">
|
||||
<i class="fa-solid fa-tv"></i> Série TV
|
||||
<div class="text-sm text-base-content/60">
|
||||
<span class="badge badge-sm badge-ghost"><i class="fa-solid fa-tv"></i> Série TV</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="anime-card-actions">
|
||||
<button class="btn btn-secondary btn-small" onclick="window.open('${escapeHtml(series.url)}', '_blank')">
|
||||
<i class="fa-solid fa-link"></i> Voir sur FS7
|
||||
</button>
|
||||
<button class="btn btn-primary btn-small" onclick="loadSeriesEpisodes('${escapeHtml(series.url)}', '${escapeHtml(series.title)}')">
|
||||
<i class="fa-solid fa-download"></i> Voir les épisodes
|
||||
</button>
|
||||
<div class="card-actions justify-end mt-3">
|
||||
<button class="btn btn-secondary btn-sm" onclick="window.open('${escapeHtml(series.url)}', '_blank')">
|
||||
<i class="fa-solid fa-link"></i> Voir sur FS7
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="loadSeriesEpisodes('${escapeHtml(series.url)}', '${escapeHtml(series.title)}')">
|
||||
<i class="fa-solid fa-download"></i> Voir les épisodes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -82,28 +80,26 @@ function renderSeriesReleaseCard(series) {
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="anime-card-horizontal release-card">
|
||||
<div class="anime-card-header">
|
||||
<div class="anime-card-title">${escapeHtml(series.title)}</div>
|
||||
</div>
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h4 class="card-title text-base">${escapeHtml(series.title)}</h4>
|
||||
|
||||
<div class="anime-card-content">
|
||||
${coverImage ? `<img src="${escapeHtml(coverImage)}" alt="" class="anime-card-image" onerror="this.style.display='none'">` : ''}
|
||||
<div class="flex gap-3 mt-1">
|
||||
${coverImage ? `<img src="${escapeHtml(coverImage)}" alt="" class="w-20 h-28 object-cover rounded-lg" onerror="this.style.display='none'">` : ''}
|
||||
|
||||
<div class="anime-card-info">
|
||||
<div class="anime-card-meta">
|
||||
<i class="fa-solid fa-tv"></i> Série TV • Nouveau
|
||||
<div class="text-sm text-base-content/60">
|
||||
<span class="badge badge-sm badge-warning"><i class="fa-solid fa-tv"></i> Série TV • Nouveau</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="anime-card-actions">
|
||||
<button class="btn btn-secondary btn-small" onclick="window.open('${escapeHtml(series.url)}', '_blank')">
|
||||
<i class="fa-solid fa-link"></i> Voir sur FS7
|
||||
</button>
|
||||
<button class="btn btn-primary btn-small" onclick="loadSeriesEpisodes('${escapeHtml(series.url)}', '${escapeHtml(series.title)}')">
|
||||
<i class="fa-solid fa-download"></i> Voir les épisodes
|
||||
</button>
|
||||
<div class="card-actions justify-end mt-3">
|
||||
<button class="btn btn-secondary btn-sm" onclick="window.open('${escapeHtml(series.url)}', '_blank')">
|
||||
<i class="fa-solid fa-link"></i> Voir sur FS7
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="loadSeriesEpisodes('${escapeHtml(series.url)}', '${escapeHtml(series.title)}')">
|
||||
<i class="fa-solid fa-download"></i> Voir les épisodes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -115,7 +111,7 @@ async function loadSeriesRecommendations() {
|
||||
const container = document.getElementById('seriesRecommendationsList');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div class="loading-spinner">Chargement des recommandations séries...</div>';
|
||||
container.innerHTML = '<div class="flex justify-center py-8"><span class="loading loading-spinner loading-md"></span><span class="ml-3 text-base-content/60">Chargement des recommandations séries...</span></div>';
|
||||
|
||||
// Search for popular series from all providers (including FS7)
|
||||
const searchTerms = ['Breaking Bad', 'Game of Thrones', 'Stranger Things', 'The Walking Dead', 'The Last of Us', 'Wednesday', 'Squid Game', 'Peaky Blinders', 'House of the Dragon', 'The Witcher'];
|
||||
@@ -141,16 +137,16 @@ async function loadSeriesRecommendations() {
|
||||
}
|
||||
|
||||
if (allSeries.length > 0) {
|
||||
container.innerHTML = `<div class="recommendations-carousel">${allSeries.map(series =>
|
||||
container.innerHTML = `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">${allSeries.map(series =>
|
||||
renderSeriesRecommendationCard(series)
|
||||
).join('')}</div>`;
|
||||
} else {
|
||||
container.innerHTML = '<div class="no-results">Aucune recommandation trouvée</div>';
|
||||
container.innerHTML = '<div class="text-center py-16 text-base-content/50">Aucune recommandation trouvée</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading series recommendations:', error);
|
||||
const container = document.getElementById('seriesRecommendationsList');
|
||||
if (container) container.innerHTML = '<div class="no-results">Erreur lors du chargement</div>';
|
||||
if (container) container.innerHTML = '<div class="text-center py-16 text-base-content/50">Erreur lors du chargement</div>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,23 +156,23 @@ async function loadAnimeReleases() {
|
||||
const container = document.getElementById('animeReleasesList');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div class="loading-spinner">Chargement des dernières sorties anime...</div>';
|
||||
container.innerHTML = '<div class="flex justify-center py-8"><span class="loading loading-spinner loading-md"></span><span class="ml-3 text-base-content/60">Chargement des dernières sorties anime...</span></div>';
|
||||
|
||||
// Use the existing releases API
|
||||
const response = await fetch(`${API_BASE}/releases/latest?limit=12`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.releases && data.releases.length > 0) {
|
||||
container.innerHTML = `<div class="recommendations-carousel">${data.releases.map(anime =>
|
||||
container.innerHTML = `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">${data.releases.map(anime =>
|
||||
renderReleaseCard(anime)
|
||||
).join('')}</div>`;
|
||||
} else {
|
||||
container.innerHTML = '<div class="no-results">Aucune sortie trouvée</div>';
|
||||
container.innerHTML = '<div class="text-center py-16 text-base-content/50">Aucune sortie trouvée</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading anime releases:', error);
|
||||
const container = document.getElementById('animeReleasesList');
|
||||
if (container) container.innerHTML = '<div class="no-results">Erreur lors du chargement</div>';
|
||||
if (container) container.innerHTML = '<div class="text-center py-16 text-base-content/50">Erreur lors du chargement</div>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +182,7 @@ async function loadSeriesReleases() {
|
||||
const container = document.getElementById('seriesReleasesList');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div class="loading-spinner">Chargement des dernières séries TV...</div>';
|
||||
container.innerHTML = '<div class="flex justify-center py-8"><span class="loading loading-spinner loading-md"></span><span class="ml-3 text-base-content/60">Chargement des dernières séries TV...</span></div>';
|
||||
|
||||
// Search for popular series from all providers (including FS7)
|
||||
const searchTerms = ['Breaking Bad', 'Game of Thrones', 'Stranger Things', 'The Walking Dead', 'The Last of Us', 'Wednesday', 'Squid Game', 'Peaky Blinders'];
|
||||
@@ -218,14 +214,14 @@ async function loadSeriesReleases() {
|
||||
}
|
||||
|
||||
if (allSeries.length > 0) {
|
||||
container.innerHTML = `<div class="releases-carousel">${allSeries.map(series =>
|
||||
container.innerHTML = `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">${allSeries.map(series =>
|
||||
renderSeriesReleaseCard(series)
|
||||
).join('')}</div>`;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="no-results">
|
||||
<div class="text-center py-16 text-base-content/50">
|
||||
<p>Aucune série trouvée</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; opacity: 0.7;">
|
||||
<p class="text-xs mt-2 opacity-70">
|
||||
Utilisez l'onglet "Recherche" pour trouver des séries spécifiques
|
||||
</p>
|
||||
</div>`;
|
||||
@@ -235,10 +231,11 @@ async function loadSeriesReleases() {
|
||||
const container = document.getElementById('seriesReleasesList');
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p><i class="fa-solid fa-xmark"></i> Erreur lors du chargement des séries</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; color: #ff6b6b;">${error.message}</p>
|
||||
<button class="btn btn-secondary btn-small" onclick="loadSeriesReleases()" style="margin-top: 10px;">
|
||||
<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 du chargement des séries</p>
|
||||
<p class="text-xs mt-2 text-error">${error.message}</p>
|
||||
<button class="btn btn-secondary btn-sm mt-3" onclick="loadSeriesReleases()">
|
||||
<i class="fa-solid fa-rotate"></i> Réessayer
|
||||
</button>
|
||||
</div>`;
|
||||
@@ -252,7 +249,7 @@ async function loadProvidersGrid() {
|
||||
const container = document.getElementById('providersGrid');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div class="loading-spinner">Chargement des fournisseurs...</div>';
|
||||
container.innerHTML = '<div class="flex justify-center py-8"><span class="loading loading-spinner loading-md"></span><span class="ml-3 text-base-content/60">Chargement des fournisseurs...</span></div>';
|
||||
|
||||
const response = await fetch(`${API_BASE}/providers`);
|
||||
const data = await response.json();
|
||||
@@ -260,65 +257,67 @@ async function loadProvidersGrid() {
|
||||
let html = '';
|
||||
|
||||
// Section Anime providers
|
||||
html += '<div class="section-header"><h3 style="margin-top: 20px;"><i class="fa-solid fa-film"></i> Sites Anime</h3></div>';
|
||||
html += '<div class="search-results">';
|
||||
html += '<div class="flex items-center gap-2 mt-5 mb-3"><h3 class="text-lg font-semibold"><i class="fa-solid fa-film"></i> Sites Anime</h3></div>';
|
||||
html += '<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">';
|
||||
|
||||
const animeProviders = Object.entries(data.anime_providers || {});
|
||||
if (animeProviders.length > 0) {
|
||||
animeProviders.forEach(([id, provider]) => {
|
||||
const domains = provider.domains || [];
|
||||
html += `
|
||||
<div class="anime-card">
|
||||
<div class="anime-card-header">
|
||||
<div class="anime-card-title">${provider.icon} ${provider.name}</div>
|
||||
</div>
|
||||
${domains.length > 0 ? `
|
||||
<div class="anime-metadata" style="margin-bottom: 12px;">
|
||||
<strong>Domaines:</strong><br>
|
||||
${domains.map(d => `<code style="background: rgba(0,217,255,0.1); padding: 2px 6px; border-radius: 4px; margin-right: 4px;">${d}</code>`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="anime-card-actions">
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h4 class="card-title text-base">${provider.icon} ${provider.name}</h4>
|
||||
${domains.length > 0 ? `
|
||||
<button class="btn btn-primary btn-small" onclick="window.open('https://${domains[0]}', '_blank')">
|
||||
<i class="fa-solid fa-link"></i> Visiter le site
|
||||
</button>
|
||||
<div class="text-sm mb-3">
|
||||
<strong>Domaines:</strong><br>
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
${domains.map(d => `<code class="badge badge-ghost badge-sm">${d}</code>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<button class="btn btn-secondary btn-small" onclick="showProviderSearch('${id}')">
|
||||
<i class="fa-solid fa-magnifying-glass"></i> Rechercher
|
||||
</button>
|
||||
<div class="card-actions justify-end">
|
||||
${domains.length > 0 ? `
|
||||
<button class="btn btn-primary btn-sm" onclick="window.open('https://${domains[0]}', '_blank')">
|
||||
<i class="fa-solid fa-link"></i> Visiter le site
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="btn btn-secondary btn-sm" onclick="showProviderSearch('${id}')">
|
||||
<i class="fa-solid fa-magnifying-glass"></i> Rechercher
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
html += '<div class="no-results">Aucun fournisseur anime disponible</div>';
|
||||
html += '<div class="col-span-full text-center py-16 text-base-content/50">Aucun fournisseur anime disponible</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// Section File hosts
|
||||
html += '<div class="section-header" style="margin-top: 40px;"><h3><i class="fa-solid fa-floppy-disk"></i> Hébergeurs de fichiers</h3></div>';
|
||||
html += '<div class="search-results">';
|
||||
html += '<div class="flex items-center gap-2 mt-10 mb-3"><h3 class="text-lg font-semibold"><i class="fa-solid fa-floppy-disk"></i> Hébergeurs de fichiers</h3></div>';
|
||||
html += '<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">';
|
||||
|
||||
const fileHosts = Object.entries(data.file_hosts || {});
|
||||
if (fileHosts.length > 0) {
|
||||
fileHosts.forEach(([id, host]) => {
|
||||
html += `
|
||||
<div class="anime-card">
|
||||
<div class="anime-card-header">
|
||||
<div class="anime-card-title">${host.icon} ${host.name}</div>
|
||||
</div>
|
||||
<div class="anime-card-actions">
|
||||
<button class="btn btn-secondary btn-small" onclick="showDownloadInfo()">
|
||||
<i class="fa-solid fa-download"></i> Télécharger un fichier
|
||||
</button>
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h4 class="card-title text-base">${host.icon} ${host.name}</h4>
|
||||
<div class="card-actions justify-end">
|
||||
<button class="btn btn-secondary btn-sm" onclick="showDownloadInfo()">
|
||||
<i class="fa-solid fa-download"></i> Télécharger un fichier
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
html += '<div class="no-results">Aucun hébergeur disponible</div>';
|
||||
html += '<div class="col-span-full text-center py-16 text-base-content/50">Aucun hébergeur disponible</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
@@ -329,10 +328,11 @@ async function loadProvidersGrid() {
|
||||
const container = document.getElementById('providersGrid');
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p><i class="fa-solid fa-xmark"></i> Erreur lors du chargement des fournisseurs</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; color: #ff6b6b;">${error.message}</p>
|
||||
<button class="btn btn-secondary btn-small" onclick="loadProvidersGrid()" style="margin-top: 10px;">
|
||||
<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 du chargement des fournisseurs</p>
|
||||
<p class="text-xs mt-2 text-error">${error.message}</p>
|
||||
<button class="btn btn-secondary btn-sm mt-3" onclick="loadProvidersGrid()">
|
||||
<i class="fa-solid fa-rotate"></i> Réessayer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+6
-6
@@ -230,42 +230,42 @@
|
||||
|
||||
<li class="mt-2">
|
||||
<button class="w-full text-left"
|
||||
:class="{ 'active': activeTab === 'home' }"
|
||||
:class="{ 'bg-primary text-primary-content rounded-lg': activeTab === 'home' }"
|
||||
@click="activeTab = 'home'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'home' } })); document.getElementById('ohm-drawer').checked = false">
|
||||
<i class="fa-solid fa-house w-5 text-center"></i> Accueil
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="w-full text-left"
|
||||
:class="{ 'active': activeTab === 'anime' }"
|
||||
:class="{ 'bg-primary text-primary-content rounded-lg': activeTab === 'anime' }"
|
||||
@click="activeTab = 'anime'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'anime' } })); document.getElementById('ohm-drawer').checked = false">
|
||||
<i class="fa-solid fa-film w-5 text-center"></i> Anime
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="w-full text-left"
|
||||
:class="{ 'active': activeTab === 'series' }"
|
||||
:class="{ 'bg-primary text-primary-content rounded-lg': activeTab === 'series' }"
|
||||
@click="activeTab = 'series'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'series' } })); document.getElementById('ohm-drawer').checked = false">
|
||||
<i class="fa-solid fa-tv w-5 text-center"></i> Séries
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="w-full text-left"
|
||||
:class="{ 'active': activeTab === 'watchlist' }"
|
||||
:class="{ 'bg-primary text-primary-content rounded-lg': activeTab === 'watchlist' }"
|
||||
@click="activeTab = 'watchlist'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'watchlist' } })); document.getElementById('ohm-drawer').checked = false">
|
||||
<i class="fa-solid fa-clipboard-list w-5 text-center"></i> Watchlist
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="w-full text-left"
|
||||
:class="{ 'active': activeTab === 'downloads' }"
|
||||
:class="{ 'bg-primary text-primary-content rounded-lg': activeTab === 'downloads' }"
|
||||
@click="activeTab = 'downloads'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'downloads' } })); document.getElementById('ohm-drawer').checked = false">
|
||||
<i class="fa-solid fa-download w-5 text-center"></i> Téléchargements
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="w-full text-left"
|
||||
:class="{ 'active': activeTab === 'settings' }"
|
||||
:class="{ 'bg-primary text-primary-content rounded-lg': activeTab === 'settings' }"
|
||||
@click="activeTab = 'settings'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'settings' } })); document.getElementById('ohm-drawer').checked = false">
|
||||
<i class="fa-solid fa-gear w-5 text-center"></i> Paramètres
|
||||
</button>
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
<header>
|
||||
<h1><i class="fa-solid fa-bolt"></i> Ohm Stream Downloader</h1>
|
||||
<p class="subtitle">Téléchargez vos vidéos, animes et séries depuis vos hébergeurs préférés</p>
|
||||
|
||||
<!-- User info and logout button -->
|
||||
<div id="userInfo" x-show="isAuthenticated" class="auth-panel" x-cloak>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="color: var(--primary); font-size: 1.2rem;"><i class="fa-solid fa-user"></i></span>
|
||||
<span style="color: var(--text-main); font-size: 14px;">Connecté en tant que <strong x-text="username" style="color: var(--primary);">-</strong></span>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-small"
|
||||
onclick="removeToken(); isAuthenticated = false"
|
||||
hx-post="/api/auth/logout"
|
||||
hx-on::after-request="window.location.href = '/login'">
|
||||
<i class="fa-solid fa-right-from-bracket"></i> Déconnexion
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Login prompt (shown when not logged in) -->
|
||||
<div id="loginPrompt" x-show="!isAuthenticated" class="auth-panel" style="justify-content: center;" x-cloak>
|
||||
<p style="color: var(--primary); margin: 0;">
|
||||
<i class="fa-solid fa-hand"></i> Bienvenue! <a href="/login" class="btn btn-secondary btn-small" style="margin-left: 10px;">Se connecter</a> pour accéder à toutes les fonctionnalités.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Tabs - Robust navigation -->
|
||||
<nav id="mainTabs" class="tabs">
|
||||
<button class="tab"
|
||||
:class="{ 'active': activeTab === 'home' }"
|
||||
@click="activeTab = 'home'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'home' } }))">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
||||
</svg>
|
||||
Accueil
|
||||
</button>
|
||||
<button class="tab"
|
||||
:class="{ 'active': activeTab === 'anime' }"
|
||||
@click="activeTab = 'anime'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'anime' } }))">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
Anime
|
||||
</button>
|
||||
<button class="tab"
|
||||
:class="{ 'active': activeTab === 'series' }"
|
||||
@click="activeTab = 'series'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'series' } }))">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 20h16a1 1 0 001-1V5a1 1 0 00-1-1H4a1 1 0 00-1 1v14a1 1 0 001 1z"></path>
|
||||
</svg>
|
||||
Série
|
||||
</button>
|
||||
<button class="tab"
|
||||
:class="{ 'active': activeTab === 'watchlist' }"
|
||||
@click="activeTab = 'watchlist'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'watchlist' } }))">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2m0 0a2 2 0 00-2 2h10a2 2 0 002-2V7a2 2 0 00-2-2"></path>
|
||||
</svg>
|
||||
Watchlist
|
||||
</button>
|
||||
<button class="tab"
|
||||
:class="{ 'active': activeTab === 'downloads' }"
|
||||
@click="activeTab = 'downloads'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'downloads' } }))">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
Téléchargements
|
||||
</button>
|
||||
<button class="tab"
|
||||
:class="{ 'active': activeTab === 'settings' }"
|
||||
@click="activeTab = 'settings'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'settings' } }))">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
Paramètres
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -23,17 +23,17 @@
|
||||
</div>
|
||||
|
||||
<!-- Error / Success Alerts -->
|
||||
<div id="authError" class="auth-error alert alert-error hidden mb-2" role="alert" aria-live="polite">
|
||||
<div id="authError" class="alert alert-error hidden mb-2" role="alert" aria-live="polite">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
<span></span>
|
||||
</div>
|
||||
<div id="authSuccess" class="auth-success alert alert-success hidden mb-2" role="status" aria-live="polite">
|
||||
<div id="authSuccess" class="alert alert-success hidden mb-2" role="status" aria-live="polite">
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form class="auth-form active" id="loginForm">
|
||||
<form id="loginForm">
|
||||
<div class="form-control mb-3">
|
||||
<label class="label" for="loginUsername">
|
||||
<span class="label-text">Nom d'utilisateur</span>
|
||||
@@ -68,7 +68,7 @@
|
||||
</form>
|
||||
|
||||
<!-- Register Form -->
|
||||
<form class="auth-form hidden" id="registerForm">
|
||||
<form class="hidden" id="registerForm">
|
||||
<div class="form-control mb-3">
|
||||
<label class="label" for="registerUsername">
|
||||
<span class="label-text">Nom d'utilisateur</span>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<li><a href="/web#anime"><i class="fa-solid fa-film"></i> Anime</a></li>
|
||||
<li><a href="/web#series"><i class="fa-solid fa-tv"></i> Série</a></li>
|
||||
<li><a href="/web#providers"><i class="fa-solid fa-box"></i> Fournisseurs</a></li>
|
||||
<li><a href="/watchlist" class="active"><i class="fa-solid fa-clipboard-list"></i> Watchlist</a></li>
|
||||
<li><a href="/watchlist" class="active bg-primary text-primary-content rounded-lg"><i class="fa-solid fa-clipboard-list"></i> Watchlist</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user