d4d8d8a3b6
- Migrated monolithic main.py to feature-scoped routers in app/routers/ - Added GEMINI.md for project context and AI instructional guidelines - Updated README.md with a comprehensive modernization plan (SQL migration, robust scraping DSL, frontend modernization) - Improved authentication with cookie support and modular JS - Updated test suite and documentation
572 lines
24 KiB
JavaScript
572 lines
24 KiB
JavaScript
/**
|
||
* 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 = getToken();
|
||
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();
|
||
const episodeCount = downloadResult.total_episodes || 'tous les';
|
||
alert(`✅ "${result.anime_title}" a été ajouté!\n\n📥 Téléchargement de la dernière saison lancé (${episodeCount} épisodes).\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; |