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:
root
2026-04-11 20:20:26 +00:00
parent 4101d98a41
commit 535005b3d5
12 changed files with 375 additions and 405 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+81 -72
View File
@@ -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,15 +395,18 @@ 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="streaming-result-episodes">
<select class="streaming-episode-select" data-provider="${provider}" data-query="${escapeHtml(query)}">
<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>
@@ -405,15 +414,16 @@ function renderStreamingResult(result, query) {
${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)">
<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="streaming-result-link" onclick="searchAnimeOnProviders('${escapeHtml(query)}'); return false;">
<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>
`;
}
@@ -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
+1 -1
View File
@@ -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'));
+4 -4
View File
@@ -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');
}
}
+69 -57
View File
@@ -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,23 +152,24 @@ 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 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 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">
<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) : ''}
@@ -173,21 +178,24 @@ function renderRecommendationCard(anime) {
</div>
${anime.synopsis ? `
<details class="anime-synopsis">
<summary><i class="fa-solid fa-book"></i> Synopsis</summary>
<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')">
<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-small" onclick="searchAnimeOnProviders('${escapeHtml(anime.title)}')">
<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,23 +209,24 @@ 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 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 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">
<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) : ''}
@@ -226,31 +235,34 @@ function renderReleaseCard(anime) {
</div>
${anime.synopsis ? `
<details class="anime-synopsis">
<summary><i class="fa-solid fa-book"></i> Synopsis</summary>
<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')">
<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-small" onclick="searchAnimeOnProviders('${escapeHtml(anime.title)}')">
<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>
`;
}
// 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)
+32 -30
View File
@@ -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 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 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="flex justify-center my-2">
<img src="${escapeHtml(coverImage)}" alt="" class="max-w-[200px] rounded-lg" onerror="this.style.display='none'">
</div>
` : ''}
<div class="anime-card-actions">
<button class="btn btn-secondary btn-small" onclick="window.open('${escapeHtml(s.url)}', '_blank')">
<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-small" onclick="loadSeriesEpisodesDirect('${escapeHtml(s.url)}', '${escapeHtml(s.title)}')">
<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)}" style="margin-top: 10px;"></div>
<div id="episodes-fs7-${encodeURIComponent(s.url)}" class="mt-2"></div>
</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
View File
@@ -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}%) &mdash; <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 &nbsp;/&nbsp;
<span style="color: #6CB4EE; font-weight: 700;">${pctS}%</span> series
<span style="color: ${primary}; font-weight: 700;">${pctA}%</span> animes &nbsp;/&nbsp;
<span style="color: ${accent}; font-weight: 700;">${pctS}%</span> series
</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 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>
`;
}
+69 -69
View File
@@ -18,32 +18,30 @@ 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>
<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 class="anime-card-actions">
<button class="btn btn-secondary btn-small" onclick="window.open('${escapeHtml(series.url)}', '_blank')">
<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-small" onclick="loadSeriesEpisodes('${escapeHtml(series.url)}', '${escapeHtml(series.title)}')">
<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,30 +80,28 @@ 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>
<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 class="anime-card-actions">
<button class="btn btn-secondary btn-small" onclick="window.open('${escapeHtml(series.url)}', '_blank')">
<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-small" onclick="loadSeriesEpisodes('${escapeHtml(series.url)}', '${escapeHtml(series.title)}')">
<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>
<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 ? `
<div class="anime-metadata" style="margin-bottom: 12px;">
<div class="text-sm mb-3">
<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 class="flex flex-wrap gap-1 mt-1">
${domains.map(d => `<code class="badge badge-ghost badge-sm">${d}</code>`).join('')}
</div>
</div>
` : ''}
<div class="anime-card-actions">
<div class="card-actions justify-end">
${domains.length > 0 ? `
<button class="btn btn-primary btn-small" onclick="window.open('https://${domains[0]}', '_blank')">
<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-small" onclick="showProviderSearch('${id}')">
<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()">
<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
View File
@@ -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>
-79
View File
@@ -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>
+4 -4
View File
@@ -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>
+1 -1
View File
@@ -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>