Files
ohm_streaming/static/js/watchlist-ui.js
T
2026-02-28 09:22:57 +00:00

571 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Watchlist UI functions
*/
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Display watchlist items
*/
async function displayWatchlist(status = null) {
const container = document.getElementById('watchlistContainer');
if (!container) return;
try {
container.innerHTML = '<div class="watchlist-loading">Chargement de la watchlist...</div>';
const items = await getWatchlist(status);
const stats = await getWatchlistStats();
if (items.length === 0) {
container.innerHTML = `
<div class="empty-watchlist">
<div style="text-align: center; padding: 60px 20px;">
<svg style="width:80px;height:80px;margin:0 auto 20px;opacity:0.3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" 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-2h2a2 2 0 012 2"></path>
</svg>
<h3 style="color: #666; margin-bottom: 10px;">Aucun anime dans votre watchlist</h3>
<p style="color: #999;">Ajoutez des animes depuis la recherche pour commencer le suivi automatique</p>
</div>
</div>
`;
return;
}
// Render stats
let statsHtml = '';
if (stats && stats.total > 0) {
statsHtml = `
<div class="watchlist-stats" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; margin-bottom: 30px;">
<div class="stat-card" style="background: rgba(0, 217, 255, 0.1); padding: 15px; border-radius: 10px; text-align: center;">
<div class="stat-value" style="font-size: 32px; font-weight: bold; color: #00d9ff;">${stats.total}</div>
<div class="stat-label" style="font-size: 12px; color: #999; text-transform: uppercase; margin-top: 5px;">Total</div>
</div>
<div class="stat-card" style="background: rgba(76, 175, 80, 0.1); padding: 15px; border-radius: 10px; text-align: center;">
<div class="stat-value" style="font-size: 32px; font-weight: bold; color: #4caf50;">${stats.active}</div>
<div class="stat-label" style="font-size: 12px; color: #999; text-transform: uppercase; margin-top: 5px;">Actifs</div>
</div>
<div class="stat-card" style="background: rgba(255, 152, 0, 0.1); padding: 15px; border-radius: 10px; text-align: center;">
<div class="stat-value" style="font-size: 32px; font-weight: bold; color: #ff9800;">${stats.paused}</div>
<div class="stat-label" style="font-size: 12px; color: #999; text-transform: uppercase; margin-top: 5px;">En pause</div>
</div>
<div class="stat-card" style="background: rgba(158, 158, 158, 0.1); padding: 15px; border-radius: 10px; text-align: center;">
<div class="stat-value" style="font-size: 32px; font-weight: bold; color: #9e9e9e;">${stats.completed}</div>
<div class="stat-label" style="font-size: 12px; color: #999; text-transform: uppercase; margin-top: 5px;">Terminés</div>
</div>
</div>
`;
}
// Render items
let itemsHtml = '';
items.forEach(item => {
const statusIcon = getStatusIcon(item.status);
const statusBadge = getStatusBadge(item.status);
const lastEpInfo = item.last_episode_downloaded > 0
? `<span style="color: #999;">Dernier épisode: ${item.last_episode_downloaded}</span>`
: '';
itemsHtml += `
<div class="watchlist-item" id="watchlist-${item.id}" style="background: rgba(255,255,255,0.05); border-radius: 12px; padding: 20px; margin-bottom: 15px; border: 1px solid rgba(255,255,255,0.1);">
<div style="display: flex; justify-content: space-between; align-items: start; gap: 20px;">
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
<h3 style="color: #fff; margin: 0; font-size: 18px;">${escapeHtml(item.anime_title)}</h3>
${statusBadge}
</div>
<div style="font-size: 13px; color: #999; margin-bottom: 8px;">
${statusIcon} ${item.provider_id}${item.lang.toUpperCase()}
</div>
${lastEpInfo ? `
<div style="font-size: 12px; color: #999; margin-bottom: 8px;">
${lastEpInfo}
</div>
` : ''}
${item.last_checked ? `
<div style="font-size: 12px; color: #666;">
Dernière vérification: ${new Date(item.last_checked).toLocaleString('fr-FR')}
</div>
` : '<div style="font-size: 12px; color: #666;">Jamais vérifié</div>'}
</div>
<div style="display: flex; flex-direction: column; gap: 8px;">
${item.status === 'active' && item.auto_download ? `
<button class="btn-secondary btn-small" onclick="handlePauseWatchlist('${item.id}')" style="padding: 6px 12px; font-size: 12px;" title="Mettre en pause">
⏸️ Pause
</button>
` : item.status === 'paused' ? `
<button class="btn-primary btn-small" onclick="handleResumeWatchlist('${item.id}')" style="padding: 6px 12px; font-size: 12px;" title="Reprendre">
▶️ Reprendre
</button>
` : ''}
<button class="btn-secondary btn-small" onclick="handleCheckItem.call(this, '${item.id}')" style="padding: 6px 12px; font-size: 12px;" title="Vérifier maintenant">
🔍 Vérifier
</button>
<button class="btn-secondary btn-small" onclick="handleDeleteWatchlist('${item.id}')" style="padding: 6px 12px; font-size: 12px; background: rgba(244, 67, 54, 0.2); border-color: rgba(244, 67, 54, 0.5);" title="Supprimer">
🗑️
</button>
</div>
</div>
${item.synopsis ? `
<details style="margin-top: 15px;">
<summary style="cursor: pointer; color: #999; font-size: 13px; padding: 5px 0;">📖 Synopsis</summary>
<p style="color: #ccc; font-size: 13px; line-height: 1.5; margin-top: 10px;">${escapeHtml(item.synopsis)}</p>
</details>
` : ''}
</div>
`;
});
container.innerHTML = statsHtml + itemsHtml;
} catch (error) {
console.error('Error loading watchlist:', error);
container.innerHTML = `
<div class="error-message" style="text-align: center; padding: 40px; color: #f44;">
❌ Erreur lors du chargement: ${error.message}
</div>
`;
}
}
/**
* Get status icon
*/
function getStatusIcon(status) {
const icons = {
'active': '✅',
'paused': '⏸️',
'completed': '✨',
'archived': '📦'
};
return icons[status] || '📌';
}
/**
* Get status badge
*/
function getStatusBadge(status) {
const badges = {
'active': '<span style="background: rgba(76, 175, 80, 0.2); color: #4caf50; padding: 3px 10px; border-radius: 12px; font-size: 11px; text-transform: uppercase;">Actif</span>',
'paused': '<span style="background: rgba(255, 152, 0, 0.2); color: #ff9800; padding: 3px 10px; border-radius: 12px; font-size: 11px; text-transform: uppercase;">En pause</span>',
'completed': '<span style="background: rgba(158, 158, 158, 0.2); color: #9e9e9e; padding: 3px 10px; border-radius: 12px; font-size: 11px; text-transform: uppercase;">Terminé</span>',
'archived': '<span style="background: rgba(33, 150, 243, 0.2); color: #2196f3; padding: 3px 10px; border-radius: 12px; font-size: 11px; text-transform: uppercase;">Archivé</span>'
};
return badges[status] || '';
}
/**
* Add anime to watchlist from search results
*/
async function handleAddToWatchlist(animeUrl, providerId) {
try {
// Decode URL if it's encoded - always work with decoded URL
let decodedUrl = animeUrl;
try {
decodedUrl = decodeURIComponent(animeUrl);
} catch (e) {
// URL might already be decoded
}
// Get anime details from the DOM or API
const response = await fetch(`${API_BASE}/anime/metadata?url=${encodeURIComponent(decodedUrl)}`);
if (!response.ok) {
throw new Error('Failed to fetch anime details');
}
const data = await response.json();
const metadata = data.metadata || {};
// Extract anime title from URL if not in metadata
let animeTitle = metadata.title || 'Unknown Anime';
if (animeTitle === 'Unknown Anime' || !animeTitle) {
// Try to extract title from URL
try {
const urlParts = decodedUrl.split('/');
// Find the anime name (usually between /catalogue/ and /saison/ or /vostfr/)
const catalogueIndex = urlParts.indexOf('catalogue');
if (catalogueIndex >= 0 && urlParts[catalogueIndex + 1]) {
animeTitle = urlParts[catalogueIndex + 1];
} else {
// Fallback: use last part
animeTitle = urlParts[urlParts.length - 2] || urlParts[urlParts.length - 1];
}
animeTitle = animeTitle.replace(/-/g, ' ').replace(/\+/g, ' ').replace(/\s+/g, ' ').trim();
// Capitalize words
animeTitle = animeTitle.replace(/\b\w/g, l => l.toUpperCase());
} catch (e) {
console.warn('Could not extract title from URL:', e);
}
}
// Normalize provider_id to use dash format (anime-sama not animesama)
let normalizedProviderId = providerId;
if (providerId === 'animesama') {
normalizedProviderId = 'anime-sama';
}
const itemData = {
anime_title: animeTitle,
anime_url: decodedUrl, // Always use decoded URL
provider_id: normalizedProviderId,
lang: 'vostfr',
auto_download: true,
quality_preference: 'auto',
poster_image: metadata.poster_image || null,
cover_image: metadata.cover_image || null,
synopsis: metadata.synopsis || null,
genres: metadata.genres || []
};
const result = await addToWatchlist(itemData);
// Trigger download of all episodes immediately
try {
const token = localStorage.getItem('auth_token');
const downloadResponse = await fetch(`${API_BASE}/watchlist/${result.id}/download-all`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (downloadResponse.ok) {
const downloadResult = await downloadResponse.json();
alert(`✅ "${result.anime_title}" a été ajouté et le téléchargement de tous les épisodes a commencé!\n\nVous recevrez automatiquement les nouveaux épisodes.`);
} else {
// Still show success even if download failed
alert(`✅ "${result.anime_title}" a été ajouté à votre watchlist!\n\nLe téléchargement automatique des nouveaux épisodes est activé.`);
}
} catch (downloadError) {
console.warn('Auto-download trigger failed:', downloadError);
alert(`✅ "${result.anime_title}" a été ajouté à votre watchlist!\n\nLe téléchargement automatique des nouveaux épisodes est activé.`);
}
// Update button to show it's already in watchlist
updateAddButton(animeUrl, true);
} catch (error) {
console.error('Error adding to watchlist:', error);
alert(`❌ Erreur: ${error.message}`);
}
}
/**
* Update add button state
*/
function updateAddButton(animeUrl, isInWatchlist) {
// Decode URL for matching
let decodedUrl = animeUrl;
try {
decodedUrl = decodeURIComponent(animeUrl);
} catch (e) {}
// Find all buttons for this anime (try both encoded and decoded)
const buttons = document.querySelectorAll(`[data-watchlist-url="${encodeURIComponent(decodedUrl)}"], [data-watchlist-url="${decodedUrl}"]`);
buttons.forEach(button => {
if (isInWatchlist) {
button.innerHTML = '✓ Suivi';
button.disabled = true;
button.style.opacity = '0.6';
} else {
button.innerHTML = '+ Suivre';
button.disabled = false;
button.style.opacity = '1';
}
});
}
/**
* Pause watchlist item
*/
async function handlePauseWatchlist(itemId) {
try {
await pauseWatchlistItem(itemId);
await displayWatchlist();
alert('✅ Anime mis en pause');
} catch (error) {
console.error('Error pausing item:', error);
alert(`❌ Erreur: ${error.message}`);
}
}
/**
* Resume watchlist item
*/
async function handleResumeWatchlist(itemId) {
try {
await resumeWatchlistItem(itemId);
await displayWatchlist();
alert('✅ Anime réactivé');
} catch (error) {
console.error('Error resuming item:', error);
alert(`❌ Erreur: ${error.message}`);
}
}
/**
* Check specific item
*/
async function handleCheckItem(itemId) {
const button = this;
const originalText = button.innerHTML;
try {
button.disabled = true;
button.innerHTML = '⏳...';
const result = await checkWatchlistItem(itemId);
if (result.new_episodes_found > 0) {
alert(`🎉 ${result.new_episodes_found} nouveau(x) épisode(s) trouvé(s)!\n\n${result.episodes_downloaded.length} téléchargé(s)`);
} else {
alert('️ Aucun nouvel épisode trouvé');
}
await displayWatchlist();
} catch (error) {
console.error('Error checking item:', error);
alert(`❌ Erreur: ${error.message}`);
} finally {
button.disabled = false;
button.innerHTML = originalText;
}
}
/**
* Delete watchlist item
*/
async function handleDeleteWatchlist(itemId) {
if (!confirm('⚠️ Êtes-vous sûr de vouloir supprimer cet anime de votre watchlist ?')) {
return;
}
try {
await deleteFromWatchlist(itemId);
await displayWatchlist();
alert('✅ Anime supprimé de la watchlist');
} catch (error) {
console.error('Error deleting item:', error);
alert(`❌ Erreur: ${error.message}`);
}
}
/**
* Check all items
*/
async function handleCheckAll() {
const button = this;
const originalText = button.innerHTML;
try {
button.disabled = true;
button.innerHTML = '⏳ Vérification...';
const result = await checkAllWatchlistItems();
alert(`✅ Vérification terminée!\n\n${result.checked} animes vérifiés\n${result.total_new_episodes} nouveaux épisodes trouvés\n${result.total_downloaded} téléchargés`);
await displayWatchlist();
} catch (error) {
console.error('Error checking all:', error);
alert(`❌ Erreur: ${error.message}`);
} finally {
button.disabled = false;
button.innerHTML = originalText;
}
}
/**
* Create settings modal HTML
*/
function createSettingsModal(settings) {
const modalHtml = `
<div id="settingsModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 1000;">
<div style="background: linear-gradient(135deg, #1e1e2e 0%, #2d1b69 100%); border-radius: 16px; padding: 30px; max-width: 500px; width: 90%; border: 1px solid rgba(0, 217, 255, 0.3);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px;">
<h2 style="margin: 0; color: #00d9ff;">⚙️ Paramètres Watchlist</h2>
<button onclick="closeSettingsModal()" style="background: none; border: none; color: #999; font-size: 24px; cursor: pointer;">×</button>
</div>
<div style="display: flex; flex-direction: column; gap: 20px;">
<!-- Check Interval -->
<div>
<label style="display: block; color: #fff; margin-bottom: 8px; font-weight: 500;">
🔄 Fréquence de vérification (heures)
</label>
<input type="number" id="checkInterval" value="${settings.check_interval_hours}" min="1" max="168"
style="width: 100%; padding: 10px; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; color: #fff; font-size: 14px;">
<p style="font-size: 12px; color: #999; margin-top: 5px;">Entre 1 et 168 heures (1 semaine)</p>
</div>
<!-- Auto-download enabled -->
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<div style="color: #fff; font-weight: 500;">📥 Téléchargement automatique</div>
<p style="font-size: 12px; color: #999; margin: 0;">Télécharger automatiquement les nouveaux épisodes</p>
</div>
<label class="switch">
<input type="checkbox" id="autoDownloadEnabled" ${settings.auto_download_enabled ? 'checked' : ''}>
<span class="slider"></span>
</label>
</div>
<!-- Max concurrent downloads -->
<div>
<label style="display: block; color: #fff; margin-bottom: 8px; font-weight: 500;">
⚡ Téléchargements simultanés max
</label>
<input type="number" id="maxConcurrent" value="${settings.max_concurrent_auto_downloads}" min="1" max="5"
style="width: 100%; padding: 10px; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; color: #fff; font-size: 14px;">
<p style="font-size: 12px; color: #999; margin-top: 5px;">Maximum 5 téléchargements en parallèle</p>
</div>
<!-- Notifications -->
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<div style="color: #fff; font-weight: 500;">🔔 Notifications</div>
<p style="font-size: 12px; color: #999; margin: 0;">Être notifié des nouveaux épisodes</p>
</div>
<label class="switch">
<input type="checkbox" id="notifyEnabled" ${settings.notify_on_new_episodes ? 'checked' : ''}>
<span class="slider"></span>
</label>
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 30px;">
<button class="btn-primary modal-action-btn" onclick="saveSettings()">
💾 Enregistrer
</button>
<button class="btn-secondary modal-action-btn" onclick="closeSettingsModal()">
Annuler
</button>
</div>
</div>
</div>
<style>
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 26px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255,255,255,0.2);
transition: .4s;
border-radius: 26px;
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #00d9ff;
}
input:checked + .slider:before {
transform: translateX(24px);
}
</style>
`;
return modalHtml;
}
/**
* Close settings modal
*/
function closeSettingsModal() {
const modal = document.getElementById('settingsModal');
if (modal) {
modal.remove();
}
}
/**
* Save settings
*/
async function saveSettings() {
try {
const checkInterval = parseInt(document.getElementById('checkInterval').value);
const autoDownloadEnabled = document.getElementById('autoDownloadEnabled').checked;
const maxConcurrent = parseInt(document.getElementById('maxConcurrent').value);
const notifyEnabled = document.getElementById('notifyEnabled').checked;
const settings = {
check_interval_hours: checkInterval,
auto_download_enabled: autoDownloadEnabled,
max_concurrent_auto_downloads: maxConcurrent,
notify_on_new_episodes: notifyEnabled
};
await updateWatchlistSettings(settings);
// Restart scheduler if it's running to apply new interval
const status = await getSchedulerStatus();
if (status.running) {
await stopScheduler();
await startScheduler();
}
closeSettingsModal();
alert('✅ Paramètres enregistrés avec succès!');
await loadSchedulerStatus();
} catch (error) {
console.error('Error saving settings:', error);
alert(`❌ Erreur: ${error.message}`);
}
}
// Make functions available globally
window.displayWatchlist = displayWatchlist;
window.handleAddToWatchlist = handleAddToWatchlist;
window.handlePauseWatchlist = handlePauseWatchlist;
window.handleResumeWatchlist = handleResumeWatchlist;
window.handleCheckItem = handleCheckItem;
window.handleDeleteWatchlist = handleDeleteWatchlist;
window.handleCheckAll = handleCheckAll;
window.createSettingsModal = createSettingsModal;
window.closeSettingsModal = closeSettingsModal;
window.saveSettings = saveSettings;