819acf04f8
Episode list: - Added 'Saison complète' header button to download all episodes at once - Added multi-select mode with checkboxes for batch episode download - Individual download buttons now show visual feedback (checkmark + reset) - Better grid/list toggle with selection state indicators Search results (anime + series): - Redesigned download dropdown with icons, descriptions, spinner on click - Smooth scale/opacity transitions on dropdown open/close - Consistent btn-success color for all download actions Series search JS: - Replaced basic <select> with scrollable episode list inline - Added 'Tout télécharger' button per series card - Replaced all alert() calls with toast notifications - Episode buttons show checkmark on successful download Anime details JS: - Added batch download button next to episode select - Fixed pre-existing lint error (escaped quote in translateSynopsis) - Standardized download icon to fa-arrow-down across all cards Recommendations + Tabs JS: - Unified download button color (btn-success) across all card types - Consistent icon (fa-arrow-down) for download actions Toast system: - Connected to existing Alpine.js toast infrastructure (show-toast events)
611 lines
25 KiB
JavaScript
611 lines
25 KiB
JavaScript
// Anime details module
|
|
|
|
// Search anime and display details
|
|
async function searchAnimeDetails(query, malId = null) {
|
|
const resultsContainer = document.getElementById('animeSearchResults');
|
|
|
|
if (!resultsContainer) return;
|
|
|
|
try {
|
|
resultsContainer.innerHTML = '<div class="flex justify-center py-8"><span class="loading loading-spinner loading-md"></span><span class="ml-3 text-base-content/60">Recherche en cours...</span></div>';
|
|
|
|
// If we have a MAL ID, fetch directly by ID, otherwise search by query
|
|
let malUrl;
|
|
if (malId) {
|
|
malUrl = `${API_BASE}/anime/mal/${malId}`;
|
|
} else {
|
|
malUrl = `${API_BASE}/anime/mal/search?q=${encodeURIComponent(query)}&limit=5`;
|
|
}
|
|
|
|
// Search MAL and get streaming results in parallel
|
|
const [malResponse, streamingData] = await Promise.allSettled([
|
|
fetch(malUrl),
|
|
searchAnime(query, 'vostfr', false)
|
|
]);
|
|
|
|
let animeData = null;
|
|
let malFound = false;
|
|
|
|
// Check MAL search results
|
|
if (malResponse.status === 'fulfilled') {
|
|
try {
|
|
// malResponse.value is the Response object from fetch
|
|
const response = malResponse.value;
|
|
|
|
// Check if the HTTP request was successful
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
console.log('MAL search response:', data);
|
|
|
|
// Handle both direct ID response and search response
|
|
if (data.anime) {
|
|
animeData = data.anime;
|
|
malFound = true;
|
|
} else if (data.mal_id) {
|
|
// Direct MAL ID response
|
|
animeData = data;
|
|
malFound = true;
|
|
}
|
|
} else {
|
|
console.warn(`MAL search returned HTTP ${response.status}`);
|
|
}
|
|
} catch (e) {
|
|
console.error('Error parsing MAL response:', e);
|
|
}
|
|
} else {
|
|
console.error('MAL search promise rejected:', malResponse.reason);
|
|
}
|
|
|
|
// Build streaming results HTML
|
|
let streamingHtml = '';
|
|
if (streamingData.status === 'fulfilled' && streamingData.value && streamingData.value.results) {
|
|
const providersData = await getProvidersInfo();
|
|
|
|
// Build results HTML
|
|
const streamingParts = [];
|
|
let hasResults = false;
|
|
|
|
// Display results from each provider - render all cards in parallel
|
|
for (const [providerId, results] of Object.entries(streamingData.value.results)) {
|
|
if (results && results.length > 0) {
|
|
hasResults = true;
|
|
const provider = providersData.anime_providers[providerId];
|
|
|
|
// Render all cards for this provider
|
|
const cardPromises = results.map((anime) => renderAnimeCard(anime, providerId, provider, 'vostfr'));
|
|
const cards = await Promise.all(cardPromises);
|
|
streamingParts.push(...cards);
|
|
}
|
|
}
|
|
|
|
// Only add header and wrapper if we have results
|
|
if (hasResults) {
|
|
streamingParts.unshift(
|
|
`<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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">`
|
|
);
|
|
streamingParts.push('</div>');
|
|
streamingHtml = streamingParts.join('');
|
|
}
|
|
}
|
|
|
|
// Display results
|
|
if (malFound && animeData) {
|
|
// We found MAL data - display anime details card
|
|
let html = renderAnimeDetails(animeData);
|
|
|
|
// Append streaming results if available
|
|
html += streamingHtml;
|
|
|
|
resultsContainer.innerHTML = html;
|
|
|
|
// Now load seasons after HTML is in DOM
|
|
if (streamingData.status === 'fulfilled' && streamingData.value && streamingData.value.results) {
|
|
loadStreamingResultsSeasons(streamingData.value.results);
|
|
}
|
|
} else {
|
|
// MAL found nothing but we have streaming results
|
|
if (streamingHtml) {
|
|
resultsContainer.innerHTML = `
|
|
<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>
|
|
${streamingHtml}
|
|
`;
|
|
|
|
// Now load seasons after HTML is in DOM
|
|
if (streamingData.status === 'fulfilled' && streamingData.value && streamingData.value.results) {
|
|
loadStreamingResultsSeasons(streamingData.value.results);
|
|
}
|
|
} else {
|
|
resultsContainer.innerHTML = `
|
|
<div class="text-center py-16 text-base-content/50">
|
|
<i class="fa-solid fa-xmark text-3xl mb-3 block"></i>
|
|
<p>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>
|
|
`;
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error searching anime details:', error);
|
|
resultsContainer.innerHTML = `
|
|
<div class="text-center py-16 text-base-content/50">
|
|
<i class="fa-solid fa-xmark text-3xl mb-3 block"></i>
|
|
<p>Erreur lors de la recherche.</p>
|
|
<p class="text-xs mt-2 text-error">${error.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Get provider search results as HTML
|
|
async function getProviderSearchResults(query) {
|
|
try {
|
|
// Use the existing searchAnime function
|
|
const data = await searchAnime(query, 'vostfr', false);
|
|
|
|
if (!data.results) {
|
|
return '';
|
|
}
|
|
|
|
// Build results HTML
|
|
const htmlParts = [];
|
|
let hasResults = false;
|
|
|
|
// Display results from each provider
|
|
for (const [providerId, results] of Object.entries(data.results)) {
|
|
if (results && results.length > 0) {
|
|
hasResults = true;
|
|
const providersData = await getProvidersInfo();
|
|
const provider = providersData.anime_providers[providerId];
|
|
|
|
// Render all cards for this provider in parallel
|
|
const cardPromises = results.map((anime) => renderAnimeCard(anime, providerId, provider, 'vostfr'));
|
|
const cards = await Promise.all(cardPromises);
|
|
htmlParts.push(...cards);
|
|
}
|
|
}
|
|
|
|
// Only add header and wrapper if we have results
|
|
if (hasResults) {
|
|
htmlParts.unshift(
|
|
`<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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">`
|
|
);
|
|
htmlParts.push('</div>');
|
|
}
|
|
|
|
return htmlParts.join('');
|
|
|
|
} catch (error) {
|
|
console.error('Error getting provider search results:', error);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
// After displaying streaming results, load seasons for Anime-Sama
|
|
async function loadStreamingResultsSeasons(providerResults) {
|
|
// providerResults should be the data.results object
|
|
let delayCounter = 0;
|
|
|
|
for (const [providerId, results] of Object.entries(providerResults)) {
|
|
if (results && results.length > 0) {
|
|
results.forEach((anime, index) => {
|
|
// Only load seasons for Anime-Sama
|
|
if (providerId === 'animesama' || (anime.url && anime.url.includes('anime-sama'))) {
|
|
// Stagger requests: 500ms delay between each anime
|
|
setTimeout(() => {
|
|
loadSeasonsForAnime(providerId, encodeURIComponent(anime.url));
|
|
}, 500 * delayCounter);
|
|
delayCounter++;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Render anime details card
|
|
function renderAnimeDetails(anime) {
|
|
const images = anime.images || {};
|
|
const imageUrl = images.jpg?.large_image_url || images.jpg?.image_url || images.webp?.large_image_url || '';
|
|
|
|
const genres = anime.genres || [];
|
|
const themes = anime.themes || [];
|
|
const studios = anime.studios || [];
|
|
const score = anime.score || 0;
|
|
const rank = anime.rank || 0;
|
|
const popularity = anime.popularity || 0;
|
|
const synopsis = anime.synopsis || '';
|
|
const related = anime.related || [];
|
|
|
|
// Generate unique ID for synopsis element
|
|
const synopsisId = `synopsis-${anime.mal_id}`;
|
|
|
|
// Filter only seasons (Sequel, Prequel)
|
|
const seasons = related.filter(r => {
|
|
const relationType = r.type?.toLowerCase() || '';
|
|
return relationType === 'sequel' || relationType === 'prequel';
|
|
});
|
|
|
|
return `
|
|
<div class="card bg-base-200 border border-base-300 shadow-lg">
|
|
<!-- Header with poster and basic info -->
|
|
<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="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="text-sm text-base-content/60">${escapeHtml(anime.title_english)}</p>
|
|
` : ''}
|
|
|
|
<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="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>` : ''}
|
|
${anime.year ? `<span><i class="fa-solid fa-calendar"></i> ${anime.year}</span>` : ''}
|
|
</div>
|
|
|
|
${studios.length > 0 ? `
|
|
<div class="text-sm mt-2 text-base-content/60">
|
|
Studio: ${studios.map(s => escapeHtml(s)).join(', ')}
|
|
</div>
|
|
` : ''}
|
|
|
|
<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-success btn-sm">
|
|
<i class="fa-solid fa-arrow-down"></i> Télécharger
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Genres and themes -->
|
|
${(genres.length > 0 || themes.length > 0) ? `
|
|
<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="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="text-sm text-base-content/80 leading-relaxed">${escapeHtml(synopsis)}</p>
|
|
</div>
|
|
` : ''}
|
|
|
|
<!-- Seasons (Sequel/Prequel) -->
|
|
${seasons.length > 0 ? `
|
|
<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>
|
|
<div class="badge badge-info badge-sm mb-1">${translateRelationType(season.type)}</div>
|
|
<div class="space-y-1">
|
|
${season.entries.map(entry => `
|
|
<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>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Load streaming results from providers
|
|
async function loadStreamingResults(query) {
|
|
const container = document.getElementById('streamingResults');
|
|
|
|
if (!container) return;
|
|
|
|
try {
|
|
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();
|
|
const animeProviders = Object.entries(providersData.anime_providers);
|
|
|
|
// Search on all providers
|
|
const results = await Promise.allSettled(
|
|
animeProviders.map(([id, provider]) =>
|
|
loadEpisodes(null, query).then(episodes => ({
|
|
provider: id,
|
|
name: provider.name,
|
|
icon: provider.icon,
|
|
episodes: episodes.episodes || []
|
|
}))
|
|
)
|
|
);
|
|
|
|
// Filter successful results
|
|
const successfulResults = results
|
|
.filter(r => r.status === 'fulfilled' && r.value.episodes.length > 0)
|
|
.map(r => r.value);
|
|
|
|
if (successfulResults.length === 0) {
|
|
container.innerHTML = `
|
|
<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;
|
|
}
|
|
|
|
// Display results
|
|
container.innerHTML = `
|
|
<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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
${successfulResults.map(result => renderStreamingResult(result, query)).join('')}
|
|
</div>
|
|
`;
|
|
|
|
} catch (error) {
|
|
console.error('Error loading streaming results:', error);
|
|
container.innerHTML = `
|
|
<div class="text-center py-16 text-base-content/50">
|
|
<i class="fa-solid fa-xmark text-3xl mb-3 block"></i>
|
|
<p>Erreur lors de la recherche des sources de streaming.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Render a single streaming result
|
|
function renderStreamingResult(result, query) {
|
|
const { provider, name, icon, episodes } = result;
|
|
|
|
return `
|
|
<div class="card bg-base-200 border border-base-300 shadow-sm">
|
|
<div class="card-body p-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<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>
|
|
|
|
<div class="flex gap-2">
|
|
<button class="btn btn-primary btn-sm flex-1 streaming-download-btn" onclick="downloadSelectedEpisode(this)">
|
|
<i class="fa-solid fa-download"></i> Télécharger
|
|
</button>
|
|
<button class="btn btn-success btn-sm streaming-download-all-btn"
|
|
onclick="downloadAllEpisodes(this, '${escapeHtml(query)}', '${escapeHtml(provider)}')"
|
|
title="Télécharger toute la saison">
|
|
<i class="fas fa-layer-group"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<a href="#" class="link link-hover text-xs mt-2" onclick="searchAnimeOnProviders('${escapeHtml(query)}'); return false;">
|
|
Voir tous les épisodes sur ${escapeHtml(name)} →
|
|
</a>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Download all episodes from a streaming result card
|
|
async function downloadAllEpisodes(button, query, provider) {
|
|
const card = button.closest('.card');
|
|
const select = card.querySelector('.streaming-episode-select');
|
|
const totalEps = select.options.length - 1; // exclude disabled options
|
|
const hasMore = select.querySelector('option[disabled]');
|
|
|
|
button.disabled = true;
|
|
const originalHtml = button.innerHTML;
|
|
button.innerHTML = '<span class="loading loading-spinner loading-xs"></span>';
|
|
|
|
let completed = 0;
|
|
const promises = [];
|
|
|
|
for (const option of select.options) {
|
|
if (!option.value || option.disabled) continue;
|
|
promises.push(
|
|
fetch(`${API_BASE}/anime/download?url=${encodeURIComponent(option.value)}`, { method: 'POST' })
|
|
.then(r => { completed++; return r; })
|
|
);
|
|
}
|
|
|
|
const results = await Promise.allSettled(promises);
|
|
const successCount = results.filter(r => r.status === 'fulfilled').length;
|
|
|
|
button.innerHTML = '<i class="fas fa-check"></i>';
|
|
showToast(`${successCount} épisodes mis en file de téléchargement`);
|
|
|
|
setTimeout(() => {
|
|
button.innerHTML = originalHtml;
|
|
button.disabled = false;
|
|
}, 4000);
|
|
|
|
// Refresh downloads list
|
|
if (typeof loadDownloads === 'function') {
|
|
loadDownloads();
|
|
}
|
|
}
|
|
|
|
// Download selected episode from streaming results
|
|
async function downloadSelectedEpisode(button) {
|
|
const select = button.parentElement.querySelector('.streaming-episode-select');
|
|
const episodeUrl = select.value;
|
|
|
|
if (!episodeUrl) {
|
|
alert('Veuillez sélectionner un épisode');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await downloadEpisode(episodeUrl);
|
|
loadDownloads();
|
|
} catch (error) {
|
|
console.error('Download error:', error);
|
|
alert('Erreur lors du téléchargement');
|
|
}
|
|
}
|
|
|
|
// Translate status
|
|
function translateStatus(status) {
|
|
const translations = {
|
|
'Airing': 'En cours',
|
|
'Finished Airing': 'Terminé',
|
|
'To Be Aired': 'À venir',
|
|
'Currently Airing': 'En cours'
|
|
};
|
|
return translations[status] || status;
|
|
}
|
|
|
|
// Translate relation type to French
|
|
function translateRelationType(type) {
|
|
const translations = {
|
|
'Sequel': 'Suite',
|
|
'Prequel': 'Préquelle',
|
|
'Spin-off': 'Spin-off',
|
|
'Side Story': 'Histoire secondaire',
|
|
'Summary': 'Résumé',
|
|
'Other': 'Autre',
|
|
'Alternative Setting': 'Version alternative',
|
|
'Full Story': 'Histoire complète'
|
|
};
|
|
return translations[type] || type;
|
|
}
|
|
|
|
// Translate synopsis to French using backend API
|
|
async function translateSynopsis(synopsisId, button) {
|
|
const synopsisElement = document.getElementById(synopsisId);
|
|
if (!synopsisElement) return;
|
|
|
|
// Get original text (use textContent to get pure text without HTML)
|
|
const originalText = synopsisElement.dataset.original || synopsisElement.textContent;
|
|
|
|
// Check if already translated
|
|
if (synopsisElement.dataset.translated === 'true') {
|
|
// Revert to original
|
|
synopsisElement.textContent = originalText;
|
|
synopsisElement.dataset.translated = 'false';
|
|
button.innerHTML = '<i class="fa-solid fa-globe"></i> Traduire en français';
|
|
return;
|
|
}
|
|
|
|
// Store original text
|
|
synopsisElement.dataset.original = originalText;
|
|
|
|
// Show loading state
|
|
button.disabled = true;
|
|
button.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Traduction...';
|
|
synopsisElement.style.opacity = '0.5';
|
|
|
|
try {
|
|
console.log('Translating text (first 100 chars):', originalText.substring(0, 100) + '...');
|
|
|
|
// Use backend translation API
|
|
const response = await fetch(`${API_BASE}/translate`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
text: originalText.substring(0, 5000)
|
|
})
|
|
});
|
|
|
|
console.log('Translation API response status:', response.status);
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
console.log('Translation successful!');
|
|
|
|
synopsisElement.textContent = data.translatedText;
|
|
synopsisElement.dataset.translated = 'true';
|
|
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);
|
|
throw new Error(errorData.detail || 'Translation failed');
|
|
}
|
|
} catch (error) {
|
|
console.error('Translation error:', error);
|
|
synopsisElement.style.opacity = '1';
|
|
|
|
// Show user-friendly error using DaisyUI alert styling
|
|
const errorMessage = document.createElement('div');
|
|
errorMessage.className = 'alert alert-error alert-sm mt-2 text-xs translation-error';
|
|
errorMessage.innerHTML = `
|
|
<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
|
|
const existingError = synopsisElement.parentElement.querySelector('.translation-error');
|
|
if (existingError) {
|
|
existingError.remove();
|
|
}
|
|
|
|
synopsisElement.parentElement.appendChild(errorMessage);
|
|
|
|
// Auto-remove error after 5 seconds
|
|
setTimeout(() => {
|
|
if (errorMessage.parentElement) {
|
|
errorMessage.remove();
|
|
}
|
|
}, 5000);
|
|
} finally {
|
|
button.disabled = false;
|
|
synopsisElement.style.opacity = '1';
|
|
}
|
|
}
|
|
|
|
// Fallback translation - kept for compatibility but no longer used
|
|
async function fallbackTranslation(text, synopsisElement, button) {
|
|
// This function is deprecated since we now use backend translation
|
|
console.log('Fallback translation called (should not happen)');
|
|
}
|