Initial commit
This commit is contained in:
629
app/static/js/discover.js
Normal file
629
app/static/js/discover.js
Normal file
@@ -0,0 +1,629 @@
|
||||
/**
|
||||
* Lycostorrent - Page Découvrir (Version simplifiée)
|
||||
* 2 catégories : Films récents / Séries en cours
|
||||
* Avec pré-cache des torrents
|
||||
*/
|
||||
|
||||
// ============================================================
|
||||
// ÉTAT GLOBAL
|
||||
// ============================================================
|
||||
|
||||
let currentCategory = 'movies';
|
||||
let currentMedia = null;
|
||||
let torrentClientEnabled = false;
|
||||
let cachedData = {}; // Cache local des données
|
||||
|
||||
// ============================================================
|
||||
// INITIALISATION
|
||||
// ============================================================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initTabs();
|
||||
checkTorrentClient();
|
||||
loadCategory('movies');
|
||||
});
|
||||
|
||||
function initTabs() {
|
||||
document.querySelectorAll('.discover-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const category = tab.dataset.category;
|
||||
|
||||
// Mettre à jour l'UI
|
||||
document.querySelectorAll('.discover-tab').forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
|
||||
// Charger la catégorie
|
||||
loadCategory(category);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function checkTorrentClient() {
|
||||
try {
|
||||
const response = await fetch('/api/torrent-client/status');
|
||||
const data = await response.json();
|
||||
torrentClientEnabled = data.success && data.enabled && data.connected;
|
||||
} catch (error) {
|
||||
torrentClientEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CHARGEMENT DES DONNÉES
|
||||
// ============================================================
|
||||
|
||||
async function loadCategory(category) {
|
||||
currentCategory = category;
|
||||
|
||||
const grid = document.getElementById('discoverGrid');
|
||||
const loader = document.getElementById('discoverLoader');
|
||||
const empty = document.getElementById('discoverEmpty');
|
||||
|
||||
// Afficher le loader
|
||||
grid.innerHTML = '';
|
||||
loader.classList.remove('hidden');
|
||||
empty.classList.add('hidden');
|
||||
hideCacheInfo();
|
||||
|
||||
// Essayer de charger depuis le cache d'abord
|
||||
try {
|
||||
const cacheResponse = await fetch(`/api/cache/data/discover/${category}`);
|
||||
const cacheData = await cacheResponse.json();
|
||||
|
||||
if (cacheData.success && cacheData.cached && cacheData.data && cacheData.data.length > 0) {
|
||||
loader.classList.add('hidden');
|
||||
cachedData[category] = cacheData.data;
|
||||
const mediaType = category === 'movies' ? 'movie' : 'tv';
|
||||
renderGrid(cacheData.data, mediaType, true);
|
||||
showCacheInfo(cacheData.timestamp);
|
||||
console.log(`📦 Discover ${category} chargé depuis le cache: ${cacheData.data.length} résultats`);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Pas de cache disponible, chargement en direct...');
|
||||
}
|
||||
|
||||
// Si pas de cache, charger en direct
|
||||
await loadCategoryLive(category);
|
||||
}
|
||||
|
||||
async function loadCategoryLive(category) {
|
||||
const grid = document.getElementById('discoverGrid');
|
||||
const loader = document.getElementById('discoverLoader');
|
||||
const empty = document.getElementById('discoverEmpty');
|
||||
|
||||
grid.innerHTML = '';
|
||||
loader.classList.remove('hidden');
|
||||
empty.classList.add('hidden');
|
||||
hideCacheInfo();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/discover/${category}`);
|
||||
const data = await response.json();
|
||||
|
||||
loader.classList.add('hidden');
|
||||
|
||||
if (data.success && data.results && data.results.length > 0) {
|
||||
cachedData[category] = data.results;
|
||||
renderGrid(data.results, data.media_type, false);
|
||||
} else {
|
||||
empty.classList.remove('hidden');
|
||||
empty.querySelector('p').textContent = data.error || 'Aucun résultat trouvé';
|
||||
}
|
||||
} catch (error) {
|
||||
loader.classList.add('hidden');
|
||||
empty.classList.remove('hidden');
|
||||
empty.querySelector('p').textContent = 'Erreur de chargement';
|
||||
console.error('Erreur:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderGrid(results, mediaType, fromCache) {
|
||||
const grid = document.getElementById('discoverGrid');
|
||||
|
||||
grid.innerHTML = results.map((item, index) => {
|
||||
const posterUrl = item.poster_path
|
||||
? `https://image.tmdb.org/t/p/w300${item.poster_path}`
|
||||
: null;
|
||||
|
||||
const title = item.title || item.name;
|
||||
const year = (item.release_date || item.first_air_date || '').substring(0, 4);
|
||||
const rating = item.vote_average ? item.vote_average.toFixed(1) : '--';
|
||||
const type = mediaType === 'movie' ? '🎬' : '📺';
|
||||
|
||||
// Indicateur de torrents disponibles (si depuis le cache)
|
||||
const torrentCount = item.torrent_count || 0;
|
||||
const torrentBadge = fromCache && torrentCount > 0
|
||||
? `<span class="torrent-badge">🧲 ${torrentCount}</span>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="discover-card" onclick="openDetail(${item.id}, '${mediaType}', ${index})">
|
||||
<div class="poster-container">
|
||||
${posterUrl
|
||||
? `<img src="${posterUrl}" alt="${escapeHtml(title)}" class="poster" loading="lazy">`
|
||||
: `<div class="poster-placeholder">${type}</div>`
|
||||
}
|
||||
<span class="rating-badge">⭐ ${rating}</span>
|
||||
${torrentBadge}
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-title" title="${escapeHtml(title)}">${escapeHtml(title)}</div>
|
||||
<div class="card-meta">
|
||||
<span class="card-year">${year || 'N/A'}</span>
|
||||
<span class="card-type">${type}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Afficher les infos du cache
|
||||
function showCacheInfo(timestamp) {
|
||||
const cacheInfo = document.getElementById('cacheInfo');
|
||||
const cacheTimestampEl = document.getElementById('cacheTimestamp');
|
||||
|
||||
if (cacheInfo && timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffMinutes = Math.floor((now - date) / 60000);
|
||||
|
||||
let timeAgo;
|
||||
if (diffMinutes < 1) {
|
||||
timeAgo = "à l'instant";
|
||||
} else if (diffMinutes < 60) {
|
||||
timeAgo = `il y a ${diffMinutes} min`;
|
||||
} else {
|
||||
const hours = Math.floor(diffMinutes / 60);
|
||||
timeAgo = `il y a ${hours}h`;
|
||||
}
|
||||
|
||||
cacheTimestampEl.textContent = timeAgo;
|
||||
cacheInfo.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function hideCacheInfo() {
|
||||
const cacheInfo = document.getElementById('cacheInfo');
|
||||
if (cacheInfo) {
|
||||
cacheInfo.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function refreshLive() {
|
||||
loadCategoryLive(currentCategory);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MODAL DÉTAILS
|
||||
// ============================================================
|
||||
|
||||
async function openDetail(id, mediaType, index) {
|
||||
const modal = document.getElementById('detailModal');
|
||||
const listEl = document.getElementById('torrentsList');
|
||||
const loadingEl = document.getElementById('torrentsLoading');
|
||||
const emptyEl = document.getElementById('torrentsEmpty');
|
||||
|
||||
// Réinitialiser
|
||||
listEl.innerHTML = '';
|
||||
loadingEl.classList.add('hidden');
|
||||
emptyEl.classList.add('hidden');
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
// Vérifier si on a les données en cache local (avec détails + torrents pré-chargés)
|
||||
const category = mediaType === 'movie' ? 'movies' : 'tv';
|
||||
const cachedItem = cachedData[category] ? cachedData[category][index] : null;
|
||||
|
||||
// Si les détails sont pré-cachés, on les utilise directement (INSTANTANÉ)
|
||||
if (cachedItem && cachedItem.details_cached) {
|
||||
console.log(`📦 Détails + torrents depuis le cache pour: ${cachedItem.title || cachedItem.name}`);
|
||||
|
||||
currentMedia = cachedItem;
|
||||
currentMedia.media_type = mediaType;
|
||||
|
||||
// Afficher les détails depuis le cache
|
||||
renderDetailFromCache(cachedItem, mediaType);
|
||||
|
||||
// Afficher les torrents depuis le cache
|
||||
if (cachedItem.torrents && cachedItem.torrents.length > 0) {
|
||||
renderTorrents(cachedItem.torrents);
|
||||
} else {
|
||||
emptyEl.classList.remove('hidden');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Sinon, fallback : charger depuis l'API (pour les items sans cache)
|
||||
try {
|
||||
const response = await fetch(`/api/discover/detail/${mediaType}/${id}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
currentMedia = data.detail;
|
||||
currentMedia.media_type = mediaType;
|
||||
renderDetail(data.detail, mediaType);
|
||||
|
||||
// Si on a des torrents pré-cachés, les afficher
|
||||
if (cachedItem && cachedItem.torrents && cachedItem.torrents.length > 0) {
|
||||
renderTorrents(cachedItem.torrents);
|
||||
} else {
|
||||
// Sinon, rechercher en direct
|
||||
searchTorrents(data.detail, mediaType);
|
||||
}
|
||||
} else {
|
||||
closeDetailModal();
|
||||
alert('Erreur lors du chargement des détails');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur:', error);
|
||||
closeDetailModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Afficher les détails depuis le cache (nouvelle fonction)
|
||||
function renderDetailFromCache(item, mediaType) {
|
||||
const title = item.title || item.name;
|
||||
const year = (item.release_date || item.first_air_date || '').substring(0, 4);
|
||||
const posterUrl = item.poster_path
|
||||
? `https://image.tmdb.org/t/p/w300${item.poster_path}`
|
||||
: '/static/icons/icon-192x192.png';
|
||||
|
||||
document.getElementById('detailPoster').src = posterUrl;
|
||||
document.getElementById('detailPoster').alt = title;
|
||||
document.getElementById('detailTitle').textContent = title;
|
||||
document.getElementById('detailYear').textContent = year;
|
||||
document.getElementById('detailRating').textContent = `⭐ ${item.vote_average ? item.vote_average.toFixed(1) : '--'}`;
|
||||
document.getElementById('detailOverview').textContent = item.overview || 'Aucune description disponible.';
|
||||
|
||||
// Genres
|
||||
const genresContainer = document.getElementById('detailGenres');
|
||||
if (item.genres && item.genres.length > 0) {
|
||||
genresContainer.innerHTML = item.genres.map(g => `<span>${g.name}</span>`).join('');
|
||||
} else {
|
||||
genresContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
// Bande-annonce YouTube
|
||||
const trailerSection = document.getElementById('detailTrailer');
|
||||
const trailerFrame = document.getElementById('trailerFrame');
|
||||
|
||||
if (item.trailer_url) {
|
||||
trailerFrame.src = item.trailer_url;
|
||||
trailerSection.classList.remove('hidden');
|
||||
} else {
|
||||
trailerFrame.src = '';
|
||||
trailerSection.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function renderDetail(detail, mediaType) {
|
||||
const title = detail.title || detail.name;
|
||||
const year = (detail.release_date || detail.first_air_date || '').substring(0, 4);
|
||||
const posterUrl = detail.poster_path
|
||||
? `https://image.tmdb.org/t/p/w300${detail.poster_path}`
|
||||
: '/static/icons/icon-192x192.png';
|
||||
|
||||
document.getElementById('detailPoster').src = posterUrl;
|
||||
document.getElementById('detailPoster').alt = title;
|
||||
document.getElementById('detailTitle').textContent = title;
|
||||
document.getElementById('detailYear').textContent = year;
|
||||
document.getElementById('detailRating').textContent = `⭐ ${detail.vote_average ? detail.vote_average.toFixed(1) : '--'}`;
|
||||
document.getElementById('detailOverview').textContent = detail.overview || 'Aucune description disponible.';
|
||||
|
||||
// Genres
|
||||
const genresContainer = document.getElementById('detailGenres');
|
||||
if (detail.genres && detail.genres.length > 0) {
|
||||
genresContainer.innerHTML = detail.genres.map(g => `<span>${g.name}</span>`).join('');
|
||||
} else {
|
||||
genresContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
// Bande-annonce YouTube
|
||||
const trailerSection = document.getElementById('detailTrailer');
|
||||
const trailerFrame = document.getElementById('trailerFrame');
|
||||
|
||||
if (detail.trailer_url) {
|
||||
trailerFrame.src = detail.trailer_url;
|
||||
trailerSection.classList.remove('hidden');
|
||||
} else {
|
||||
trailerFrame.src = '';
|
||||
trailerSection.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeDetailModal() {
|
||||
document.getElementById('detailModal').classList.add('hidden');
|
||||
// Arrêter la vidéo YouTube
|
||||
document.getElementById('trailerFrame').src = '';
|
||||
currentMedia = null;
|
||||
}
|
||||
|
||||
// Fermer le modal en cliquant à l'extérieur
|
||||
document.getElementById('detailModal')?.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'detailModal') {
|
||||
closeDetailModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Fermer avec Escape
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeDetailModal();
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// RECHERCHE DE TORRENTS (fallback si pas en cache)
|
||||
// ============================================================
|
||||
|
||||
async function searchTorrents(detail, mediaType) {
|
||||
const loadingEl = document.getElementById('torrentsLoading');
|
||||
const listEl = document.getElementById('torrentsList');
|
||||
const emptyEl = document.getElementById('torrentsEmpty');
|
||||
|
||||
loadingEl.classList.remove('hidden');
|
||||
listEl.innerHTML = '';
|
||||
emptyEl.classList.add('hidden');
|
||||
|
||||
const title = detail.title || detail.name;
|
||||
const originalTitle = detail.original_title || detail.original_name || '';
|
||||
const year = (detail.release_date || detail.first_air_date || '').substring(0, 4);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/discover/search-torrents', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: title,
|
||||
original_title: originalTitle,
|
||||
year: year,
|
||||
media_type: mediaType,
|
||||
tmdb_id: detail.id
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
loadingEl.classList.add('hidden');
|
||||
|
||||
if (data.success && data.results && data.results.length > 0) {
|
||||
renderTorrents(data.results);
|
||||
} else {
|
||||
emptyEl.classList.remove('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
loadingEl.classList.add('hidden');
|
||||
emptyEl.classList.remove('hidden');
|
||||
console.error('Erreur recherche torrents:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderTorrents(torrents) {
|
||||
const listEl = document.getElementById('torrentsList');
|
||||
|
||||
listEl.innerHTML = torrents.slice(0, 20).map((torrent, index) => {
|
||||
const size = torrent.Size ? formatSize(torrent.Size) : 'N/A';
|
||||
const seeds = torrent.Seeders || 0;
|
||||
const quality = torrent.parsed?.quality || '';
|
||||
const tracker = torrent.Tracker || torrent.TrackerName || 'Unknown';
|
||||
|
||||
const magnetUrl = torrent.MagnetUri || '';
|
||||
const downloadUrl = torrent.Link || '';
|
||||
const detailsUrl = torrent.Details || torrent.Guid || '';
|
||||
const torrentUrl = magnetUrl || downloadUrl;
|
||||
|
||||
return `
|
||||
<div class="torrent-item">
|
||||
<div class="torrent-info">
|
||||
<div class="torrent-name" title="${escapeHtml(torrent.Title)}">${escapeHtml(torrent.Title)}</div>
|
||||
<div class="torrent-meta">
|
||||
<span class="tracker">📡 ${escapeHtml(tracker)}</span>
|
||||
<span class="size">💾 ${size}</span>
|
||||
<span class="seeds">🌱 ${seeds}</span>
|
||||
${quality ? `<span class="quality">${escapeHtml(quality)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="torrent-actions">
|
||||
${detailsUrl ? `<a href="${detailsUrl}" target="_blank" class="btn-link" title="Voir sur le tracker">🔗</a>` : ''}
|
||||
${magnetUrl ? `<a href="${magnetUrl}" class="btn-magnet" title="Magnet">🧲</a>` : ''}
|
||||
${downloadUrl ? `<a href="${downloadUrl}" target="_blank" class="btn-download" title="Télécharger .torrent">⬇️</a>` : ''}
|
||||
${torrentClientEnabled && torrentUrl ?
|
||||
`<button class="btn-send" id="send-btn-${index}" onclick="handleSendToClient('${escapeHtml(torrentUrl)}', 'send-btn-${index}')" title="Envoyer au client">📥</button>`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function handleSendToClient(url, buttonId) {
|
||||
const button = document.getElementById(buttonId);
|
||||
sendToClient(url, button);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ENVOI AU CLIENT TORRENT
|
||||
// ============================================================
|
||||
|
||||
async function sendToClient(url, buttonElement) {
|
||||
if (!url) return;
|
||||
showTorrentOptionsModal(url, buttonElement);
|
||||
}
|
||||
|
||||
async function showTorrentOptionsModal(url, button) {
|
||||
let modal = document.getElementById('torrentOptionsModal');
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'torrentOptionsModal';
|
||||
modal.className = 'torrent-options-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="torrent-options-content">
|
||||
<h3>📥 Options de téléchargement</h3>
|
||||
<div class="torrent-option-group">
|
||||
<label for="torrentCategory">Catégorie</label>
|
||||
<select id="torrentCategory">
|
||||
<option value="">-- Aucune --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="torrent-option-group">
|
||||
<label for="torrentSavePath">Dossier (optionnel)</label>
|
||||
<input type="text" id="torrentSavePath" placeholder="/chemin/vers/dossier">
|
||||
</div>
|
||||
<div class="torrent-option-group checkbox-group">
|
||||
<input type="checkbox" id="torrentPaused">
|
||||
<label for="torrentPaused">Démarrer en pause</label>
|
||||
</div>
|
||||
<div class="torrent-options-buttons">
|
||||
<button class="btn-cancel" onclick="closeTorrentOptionsModal()">Annuler</button>
|
||||
<button class="btn-confirm" id="confirmTorrentAdd">Envoyer</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) closeTorrentOptionsModal();
|
||||
});
|
||||
}
|
||||
|
||||
const categorySelect = document.getElementById('torrentCategory');
|
||||
const savePathInput = document.getElementById('torrentSavePath');
|
||||
categorySelect.innerHTML = '<option value="">-- Chargement... --</option>';
|
||||
|
||||
let categoriesWithPaths = {};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/torrent-client/categories');
|
||||
const data = await response.json();
|
||||
|
||||
categorySelect.innerHTML = '<option value="">-- Aucune --</option>';
|
||||
if (data.success && data.categories) {
|
||||
data.categories.forEach(cat => {
|
||||
categorySelect.innerHTML += `<option value="${escapeHtml(cat)}">${escapeHtml(cat)}</option>`;
|
||||
});
|
||||
categoriesWithPaths = data.custom_categories || {};
|
||||
}
|
||||
} catch (error) {
|
||||
categorySelect.innerHTML = '<option value="">-- Aucune --</option>';
|
||||
}
|
||||
|
||||
categorySelect.onchange = () => {
|
||||
const selectedCat = categorySelect.value;
|
||||
if (selectedCat && categoriesWithPaths[selectedCat]) {
|
||||
savePathInput.value = categoriesWithPaths[selectedCat];
|
||||
} else {
|
||||
savePathInput.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
savePathInput.value = '';
|
||||
document.getElementById('torrentPaused').checked = false;
|
||||
|
||||
const confirmBtn = document.getElementById('confirmTorrentAdd');
|
||||
confirmBtn.onclick = async () => {
|
||||
const category = document.getElementById('torrentCategory').value;
|
||||
const savePath = document.getElementById('torrentSavePath').value.trim();
|
||||
const paused = document.getElementById('torrentPaused').checked;
|
||||
|
||||
closeTorrentOptionsModal();
|
||||
await doSendToTorrentClient(url, button, category, savePath, paused);
|
||||
};
|
||||
|
||||
modal.classList.add('visible');
|
||||
}
|
||||
|
||||
function closeTorrentOptionsModal() {
|
||||
const modal = document.getElementById('torrentOptionsModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
async function doSendToTorrentClient(url, button, category, savePath, paused) {
|
||||
if (button) {
|
||||
button.textContent = '⏳';
|
||||
button.disabled = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const body = { url: url };
|
||||
if (category) body.category = category;
|
||||
if (savePath) body.save_path = savePath;
|
||||
if (paused) body.paused = paused;
|
||||
|
||||
const response = await fetch('/api/torrent-client/add', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
if (button) {
|
||||
button.textContent = '✅';
|
||||
setTimeout(() => {
|
||||
button.textContent = '📥';
|
||||
button.disabled = false;
|
||||
}, 2000);
|
||||
}
|
||||
showToast('Torrent envoyé !', 'success');
|
||||
} else {
|
||||
if (button) {
|
||||
button.textContent = '❌';
|
||||
setTimeout(() => {
|
||||
button.textContent = '📥';
|
||||
button.disabled = false;
|
||||
}, 2000);
|
||||
}
|
||||
showToast(data.error || 'Erreur', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (button) {
|
||||
button.textContent = '❌';
|
||||
setTimeout(() => {
|
||||
button.textContent = '📥';
|
||||
button.disabled = false;
|
||||
}, 2000);
|
||||
}
|
||||
showToast('Erreur de connexion', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UTILITAIRES
|
||||
// ============================================================
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (!bytes) return 'N/A';
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
let toast = document.getElementById('toast');
|
||||
if (!toast) {
|
||||
toast = document.createElement('div');
|
||||
toast.id = 'toast';
|
||||
toast.className = 'toast';
|
||||
document.body.appendChild(toast);
|
||||
}
|
||||
|
||||
toast.textContent = message;
|
||||
toast.className = `toast ${type}`;
|
||||
toast.classList.remove('hidden');
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('hidden');
|
||||
}, 3000);
|
||||
}
|
||||
Reference in New Issue
Block a user