614 lines
29 KiB
HTML
614 lines
29 KiB
HTML
{% extends "panoramax/base.html" %}
|
|
{% load i18n %}
|
|
{% load static %}
|
|
|
|
{% block head %}
|
|
{{ block.super }}
|
|
<style>
|
|
.upload-container {
|
|
max-width: 1200px;
|
|
margin: 1.5rem auto;
|
|
padding: 0 1rem;
|
|
animation: fadeIn 0.4s ease-out;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(10px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.glass-card {
|
|
background: rgba(15, 23, 42, 0.85);
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: 16px;
|
|
color: #f8fafc;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
|
margin-bottom: 2rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.card-header-gradient {
|
|
background: linear-gradient(135deg, #1e293b, #0f172a);
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
padding: 1.25rem 1.5rem;
|
|
}
|
|
|
|
.card-body-content {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.form-label {
|
|
font-weight: 500;
|
|
color: #94a3b8;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.form-control, .form-select {
|
|
background-color: rgba(30, 41, 59, 0.7);
|
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
color: #f8fafc;
|
|
border-radius: 8px;
|
|
padding: 0.6rem 0.9rem;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.form-control:focus, .form-select:focus {
|
|
background-color: rgba(30, 41, 59, 0.9);
|
|
border-color: #3b82f6;
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
|
|
color: #f8fafc;
|
|
}
|
|
|
|
.form-control::placeholder {
|
|
color: #94a3b8;
|
|
opacity: 0.65;
|
|
}
|
|
|
|
.form-check-input {
|
|
background-color: rgba(30, 41, 59, 0.7);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
.form-check-input:checked {
|
|
background-color: #3b82f6;
|
|
border-color: #3b82f6;
|
|
}
|
|
|
|
.btn-gradient {
|
|
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
|
border: none;
|
|
color: white;
|
|
font-weight: 600;
|
|
padding: 0.7rem 1.8rem;
|
|
border-radius: 8px;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 12px rgba(29, 78, 216, 0.3);
|
|
}
|
|
|
|
.btn-gradient:hover {
|
|
background: linear-gradient(135deg, #2563eb, #1e40af);
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 6px 16px rgba(29, 78, 216, 0.4);
|
|
}
|
|
|
|
/* Upload Type Selection Cards */
|
|
.type-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.type-card {
|
|
background: rgba(30, 41, 59, 0.5);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: 12px;
|
|
padding: 1rem;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.type-card:hover {
|
|
background: rgba(30, 41, 59, 0.8);
|
|
border-color: rgba(255, 255, 255, 0.2);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.type-card.active {
|
|
background: rgba(59, 130, 246, 0.15);
|
|
border-color: #3b82f6;
|
|
box-shadow: 0 0 0 1px #3b82f6;
|
|
}
|
|
|
|
.type-card i {
|
|
font-size: 1.8rem;
|
|
margin-bottom: 0.5rem;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.type-card.active i {
|
|
color: #3b82f6;
|
|
}
|
|
|
|
.type-card .title {
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.2rem;
|
|
}
|
|
|
|
.type-card .desc {
|
|
font-size: 0.75rem;
|
|
color: #64748b;
|
|
}
|
|
|
|
/* Status Badges */
|
|
.status-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
padding: 4px 10px;
|
|
border-radius: 20px;
|
|
}
|
|
|
|
.status-pending {
|
|
background: rgba(234, 179, 8, 0.15);
|
|
color: #f59e0b;
|
|
border: 1px solid rgba(245, 158, 11, 0.25);
|
|
}
|
|
|
|
.status-processing {
|
|
background: rgba(59, 130, 246, 0.15);
|
|
color: #60a5fa;
|
|
border: 1px solid rgba(96, 165, 250, 0.25);
|
|
}
|
|
|
|
.status-completed {
|
|
background: rgba(16, 185, 129, 0.15);
|
|
color: #34d399;
|
|
border: 1px solid rgba(52, 211, 153, 0.25);
|
|
}
|
|
|
|
.status-failed {
|
|
background: rgba(239, 68, 68, 0.15);
|
|
color: #f87171;
|
|
border: 1px solid rgba(248, 113, 113, 0.25);
|
|
}
|
|
|
|
/* Table styling */
|
|
.table-dark-custom {
|
|
width: 100%;
|
|
margin-bottom: 0;
|
|
color: #f8fafc;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
}
|
|
|
|
.table-dark-custom th {
|
|
background-color: rgba(30, 41, 59, 0.6);
|
|
border-bottom: 2px solid rgba(255, 255, 255, 0.08);
|
|
color: #94a3b8;
|
|
font-weight: 600;
|
|
padding: 0.8rem 1rem;
|
|
text-transform: uppercase;
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.table-dark-custom td {
|
|
padding: 1rem;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.table-dark-custom tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.progress {
|
|
height: 6px;
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-bar-animated {
|
|
animation: progress-bar-stripes 1s linear infinite;
|
|
}
|
|
</style>
|
|
{% endblock head %}
|
|
|
|
{% block main %}
|
|
<div id="main" class="d-flex">
|
|
<div id="content-wrapper" class="flex-grow-1" style="height: calc(100vh - 56px); overflow-y: auto; background: #0f172a; width: 100%;">
|
|
<!-- Barre de navigation Panoramax intégrée dans la page -->
|
|
<div class="panoramax-header border-bottom border-secondary" style="padding: 1.25rem 2rem; background: rgba(15, 23, 42, 0.95);">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h1 class="h4 font-weight-bold text-white mb-0" style="display: flex; align-items: center; gap: 8px;">
|
|
<i class="bi bi-images text-primary"></i>
|
|
{% translate "Photos 360" %}
|
|
</h1>
|
|
|
|
<div class="panoramax-nav-links" style="display: flex; gap: 0.5rem;">
|
|
<a href="{% url 'panoramax:index' %}" class="btn btn-sm btn-outline-light">
|
|
<i class="bi bi-map me-1"></i>{% translate "Carte" %}
|
|
</a>
|
|
{% if user.config.panoramax_can_upload %}
|
|
<a href="{% url 'panoramax:upload' %}" class="btn btn-sm btn-primary">
|
|
<i class="bi bi-cloud-upload me-1"></i>{% translate "Importer" %}
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="upload-container">
|
|
|
|
{% if messages %}
|
|
<div class="mb-4">
|
|
{% for message in messages %}
|
|
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}{{ message.tags }}{% endif %} alert-dismissible fade show bg-dark text-white border-secondary" role="alert">
|
|
{{ message }}
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="row">
|
|
<!-- Form Section -->
|
|
<div class="col-lg-5">
|
|
<div class="glass-card">
|
|
<div class="card-header-gradient">
|
|
<h5 class="mb-0 fw-bold">{% translate "Nouvel Upload" %}</h5>
|
|
</div>
|
|
<div class="card-body-content">
|
|
<form method="POST" enctype="multipart/form-data" id="uploadForm">
|
|
{% csrf_token %}
|
|
|
|
<div class="mb-3">
|
|
<label for="id_title" class="form-label">{% translate "Description (facultatif)" %}</label>
|
|
<input type="text" id="id_title" name="title" class="form-control" placeholder="Ex: Rénovation Place Flagey">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">{% translate "Format d'upload" %}</label>
|
|
<div class="type-grid">
|
|
<div class="type-card active" data-type="files" onclick="selectUploadType('files')">
|
|
<i class="bi bi-images"></i>
|
|
<div class="title">{% translate "Sélection" %}</div>
|
|
<div class="desc">{% translate "Images JPEG" %}</div>
|
|
</div>
|
|
<div class="type-card" data-type="folder" onclick="selectUploadType('folder')">
|
|
<i class="bi bi-folder-symlink"></i>
|
|
<div class="title">{% translate "Dossier" %}</div>
|
|
<div class="desc">{% translate "Répertoire entier" %}</div>
|
|
</div>
|
|
<div class="type-card" data-type="zip" onclick="selectUploadType('zip')">
|
|
<i class="bi bi-file-earmark-zip"></i>
|
|
<div class="title">{% translate "ZIP" %}</div>
|
|
<div class="desc">{% translate "Archive .zip" %}</div>
|
|
</div>
|
|
<div class="type-card" data-type="video" onclick="selectUploadType('video')">
|
|
<i class="bi bi-camera-video"></i>
|
|
<div class="title">{% translate "Vidéo" %}</div>
|
|
<div class="desc">{% translate "1 photo / 10m" %}</div>
|
|
</div>
|
|
</div>
|
|
<input type="hidden" id="id_upload_type" name="upload_type" value="files">
|
|
</div>
|
|
|
|
<div class="mb-3" id="mainFileInputContainer">
|
|
<label for="id_uploaded_file" class="form-label" id="fileInputLabel">{% translate "Fichiers images (JPEG)" %}</label>
|
|
<input type="file" id="id_uploaded_file" name="uploaded_file" class="form-control" accept="image/jpeg,image/jpg" multiple required>
|
|
<small class="text-muted d-block mt-1" id="fileHelpText">{% translate "Sélectionnez une ou plusieurs photos JPEG géolocalisées." %}</small>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="id_gpx_file" class="form-label">{% translate "Fichier GPX (Facultatif)" %}</label>
|
|
<input type="file" id="id_gpx_file" name="gpx_file" class="form-control" accept=".gpx">
|
|
<small class="text-muted d-block mt-1" id="gpxHelpText">{% translate "Utilisé pour recaler le positionnement des photos ou vidéos." %}</small>
|
|
</div>
|
|
|
|
<div class="mb-4" id="cameraOffsetContainer">
|
|
<label for="id_camera_offset" class="form-label">{% translate "Orientation de la caméra (Offset)" %}</label>
|
|
<select id="id_camera_offset" name="camera_offset" class="form-select bg-dark text-white border-secondary">
|
|
<option value="0">{% translate "Droit devant (0°)" %}</option>
|
|
<option value="90">{% translate "Vers la droite (90°)" %}</option>
|
|
<option value="180">{% translate "Vers l'arrière (180°)" %}</option>
|
|
<option value="270">{% translate "Vers la gauche (270°)" %}</option>
|
|
</select>
|
|
<small class="text-muted d-block mt-1">{% translate "Spécifiez la direction de la caméra par rapport au sens de déplacement du véhicule." %}</small>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-gradient w-100 py-2">
|
|
<i class="bi bi-cloud-arrow-up me-2"></i>{% translate "Lancer le traitement" %}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- History/Dashboard Section -->
|
|
<div class="col-lg-7">
|
|
<div class="glass-card">
|
|
<div class="card-header-gradient">
|
|
<h5 class="mb-0 fw-bold">{% translate "Suivi des Traitements" %}</h5>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-dark-custom" id="uploadsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>{% translate "Tâche" %}</th>
|
|
<th>{% translate "Type" %}</th>
|
|
<th>{% translate "Date" %}</th>
|
|
<th>{% translate "Progression" %}</th>
|
|
<th>{% translate "Statut" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for upload in uploads %}
|
|
<tr id="upload-row-{{ upload.id }}" data-upload-id="{{ upload.id }}" data-status="{{ upload.status }}">
|
|
<td>
|
|
<div class="font-weight-bold">{{ upload.title }}</div>
|
|
{% if upload.error_message %}
|
|
<div class="small text-danger error-msg" style="max-width: 250px;">{{ upload.error_message }}</div>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-secondary">
|
|
{% if upload.upload_type == 'video' %}{% translate "Vidéo" %}
|
|
{% elif upload.upload_type == 'zip' %}{% translate "Archive ZIP" %}
|
|
{% elif upload.upload_type == 'folder' %}{% translate "Dossier" %}
|
|
{% else %}{% translate "Photos" %}{% endif %}
|
|
</span>
|
|
</td>
|
|
<td class="small text-muted">{{ upload.created_at|date:"d/m/Y H:i" }}</td>
|
|
<td style="width: 140px;">
|
|
<div class="progress-container">
|
|
<div class="d-flex justify-content-between small text-muted mb-1">
|
|
<span class="progress-percent">{{ upload.progress }}%</span>
|
|
</div>
|
|
<div class="progress">
|
|
<div class="progress-bar {% if upload.status == 'processing' %}progress-bar-striped progress-bar-animated bg-primary{% elif upload.status == 'completed' %}bg-success{% elif upload.status == 'failed' %}bg-danger{% else %}bg-warning{% endif %}"
|
|
role="progressbar"
|
|
style="width: {{ upload.progress }}%"
|
|
aria-valuenow="{{ upload.progress }}"
|
|
aria-valuemin="0"
|
|
aria-valuemax="100">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="status-badge status-{{ upload.status }}">
|
|
{% if upload.status == 'processing' %}<div class="spinner-border spinner-border-sm text-primary" role="status" style="width: 10px; height: 10px;"></div>{% endif %}
|
|
<span class="status-text">{{ upload.get_status_display }}</span>
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="5" class="text-center py-4 text-muted">
|
|
<i class="bi bi-info-circle fs-4 d-block mb-2"></i>
|
|
{% translate "Aucun upload enregistré pour le moment." %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function selectUploadType(type) {
|
|
document.querySelectorAll('.type-card').forEach(card => card.classList.remove('active'));
|
|
document.querySelector(`.type-card[data-type="${type}"]`).classList.add('active');
|
|
document.getElementById('id_upload_type').value = type;
|
|
|
|
const fileInput = document.getElementById('id_uploaded_file');
|
|
const fileLabel = document.getElementById('fileInputLabel');
|
|
const helpText = document.getElementById('fileHelpText');
|
|
const gpxInput = document.getElementById('id_gpx_file');
|
|
|
|
// Reset attributes
|
|
fileInput.removeAttribute('webkitdirectory');
|
|
fileInput.removeAttribute('directory');
|
|
fileInput.removeAttribute('multiple');
|
|
fileInput.value = '';
|
|
|
|
if (type === 'video') {
|
|
fileLabel.textContent = "{% translate 'Fichier Vidéo (MP4, AVI, 360...)' %}";
|
|
helpText.textContent = "{% translate 'Téléversez un fichier vidéo de votre parcours. Si la vidéo ne contient pas de télémétrie GPS intégrée, un tracé GPX est requis.' %}";
|
|
fileInput.setAttribute('accept', 'video/*,.360');
|
|
gpxInput.removeAttribute('required');
|
|
} else if (type === 'zip') {
|
|
fileLabel.textContent = "{% translate 'Archive ZIP' %}";
|
|
helpText.textContent = "{% translate 'Téléversez un fichier ZIP contenant l\'ensemble de vos photos JPEG.' %}";
|
|
fileInput.setAttribute('accept', '.zip');
|
|
gpxInput.removeAttribute('required');
|
|
} else if (type === 'folder') {
|
|
fileLabel.textContent = "{% translate 'Dossier d\'images' %}";
|
|
helpText.textContent = "{% translate 'Sélectionnez un répertoire complet de photos JPEG.' %}";
|
|
fileInput.setAttribute('accept', 'image/jpeg,image/jpg');
|
|
fileInput.setAttribute('webkitdirectory', '');
|
|
fileInput.setAttribute('directory', '');
|
|
fileInput.setAttribute('multiple', '');
|
|
gpxInput.removeAttribute('required');
|
|
} else {
|
|
fileLabel.textContent = "{% translate 'Fichiers images (JPEG)' %}";
|
|
helpText.textContent = "{% translate 'Sélectionnez une ou plusieurs photos JPEG.' %}";
|
|
fileInput.setAttribute('accept', 'image/jpeg,image/jpg');
|
|
fileInput.setAttribute('multiple', '');
|
|
gpxInput.removeAttribute('required');
|
|
}
|
|
}
|
|
|
|
// Dynamic updates
|
|
function startStatusPolling() {
|
|
setInterval(() => {
|
|
const rows = document.querySelectorAll('tr[data-upload-id]');
|
|
rows.forEach(row => {
|
|
const status = row.getAttribute('data-status');
|
|
const isPanoReady = row.getAttribute('data-panoramax-ready') === 'true';
|
|
if (status === 'pending' || status === 'processing' || (status === 'completed' && !isPanoReady)) {
|
|
const id = row.getAttribute('data-upload-id');
|
|
fetch(`/panoramax/upload/${id}/status/`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
row.setAttribute('data-status', data.status);
|
|
|
|
const progressBar = row.querySelector('.progress-bar');
|
|
const progressPercent = row.querySelector('.progress-percent');
|
|
const badge = row.querySelector('.status-badge');
|
|
const statusText = row.querySelector('.status-text');
|
|
|
|
// Check if Panoramax is processing
|
|
if (data.status === 'completed' && data.panoramax_status) {
|
|
const pStatus = data.panoramax_status;
|
|
if (pStatus.ready) {
|
|
// Fully ready and published on Panoramax!
|
|
row.setAttribute('data-panoramax-ready', 'true');
|
|
progressPercent.textContent = '100%';
|
|
progressBar.style.width = '100%';
|
|
progressBar.setAttribute('aria-valuenow', 100);
|
|
progressBar.className = 'progress-bar bg-success';
|
|
progressBar.style.backgroundColor = '';
|
|
|
|
badge.className = 'status-badge status-completed';
|
|
statusText.innerHTML = '{% translate "Publié sur Panoramax" %}';
|
|
} else {
|
|
// Panoramax is actively processing photos
|
|
progressPercent.textContent = pStatus.pct + '% (Panoramax)';
|
|
progressBar.style.width = pStatus.pct + '%';
|
|
progressBar.setAttribute('aria-valuenow', pStatus.pct);
|
|
progressBar.className = 'progress-bar progress-bar-striped progress-bar-animated';
|
|
progressBar.style.backgroundColor = '#0d9488'; // sleek teal color
|
|
|
|
badge.className = 'status-badge status-processing';
|
|
const subStatusText = pStatus.preparing > 0 ? '{% translate "Panoramax : Anonymisation" %}' : '{% translate "Panoramax : Traitement" %}';
|
|
statusText.innerHTML = `
|
|
<div class="spinner-border spinner-border-sm text-info me-1" role="status" style="width: 10px; height: 10px;"></div>
|
|
<span>${subStatusText} (${pStatus.prepared}/${pStatus.total})</span>
|
|
`;
|
|
|
|
// Display info if preparing (CPU blurring)
|
|
let infoDiv = row.querySelector('.pano-info-msg');
|
|
if (pStatus.preparing > 0) {
|
|
if (!infoDiv) {
|
|
infoDiv = document.createElement('div');
|
|
infoDiv.className = 'small text-info pano-info-msg mt-1';
|
|
infoDiv.style.maxWidth = '250px';
|
|
infoDiv.innerHTML = `<i class="bi bi-shield-shaded me-1"></i>{% translate "Anonymisation (visages/plaques) en cours (CPU)..." %}`;
|
|
row.querySelector('td').appendChild(infoDiv);
|
|
}
|
|
} else if (infoDiv) {
|
|
infoDiv.remove();
|
|
}
|
|
|
|
// Display warning if there are broken photos
|
|
let warnDiv = row.querySelector('.pano-warn-msg');
|
|
if ((pStatus.broken > 0 || pStatus.rejected > 0) && !warnDiv) {
|
|
warnDiv = document.createElement('div');
|
|
warnDiv.className = 'small text-warning pano-warn-msg mt-1';
|
|
warnDiv.style.maxWidth = '250px';
|
|
warnDiv.innerHTML = `<i class="bi bi-exclamation-triangle me-1"></i>${pStatus.broken} {% translate "photos illisibles" %}`;
|
|
row.querySelector('td').appendChild(warnDiv);
|
|
}
|
|
}
|
|
} else {
|
|
// Default behavior (local processing phase)
|
|
progressPercent.textContent = data.progress + '%';
|
|
progressBar.style.width = data.progress + '%';
|
|
progressBar.setAttribute('aria-valuenow', data.progress);
|
|
progressBar.style.backgroundColor = '';
|
|
|
|
progressBar.className = 'progress-bar';
|
|
if (data.status === 'processing') {
|
|
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated', 'bg-primary');
|
|
} else if (data.status === 'completed') {
|
|
progressBar.classList.add('bg-success');
|
|
row.setAttribute('data-panoramax-ready', 'true');
|
|
} else if (data.status === 'failed') {
|
|
progressBar.classList.add('bg-danger');
|
|
row.setAttribute('data-panoramax-ready', 'true');
|
|
} else {
|
|
progressBar.classList.add('bg-warning');
|
|
}
|
|
|
|
badge.className = `status-badge status-${data.status}`;
|
|
let loaderHtml = '';
|
|
if (data.status === 'processing') {
|
|
loaderHtml = '<div class="spinner-border spinner-border-sm text-primary me-1" role="status" style="width: 10px; height: 10px;"></div>';
|
|
}
|
|
statusText.innerHTML = loaderHtml + data.status_display;
|
|
}
|
|
|
|
// Update error messages if failed
|
|
let errorDiv = row.querySelector('.error-msg');
|
|
if (data.status === 'failed' && data.error_message) {
|
|
if (!errorDiv) {
|
|
errorDiv = document.createElement('div');
|
|
errorDiv.className = 'small text-danger error-msg mt-1';
|
|
errorDiv.style.maxWidth = '250px';
|
|
row.querySelector('td').appendChild(errorDiv);
|
|
}
|
|
errorDiv.textContent = data.error_message;
|
|
} else if (errorDiv && data.status !== 'failed') {
|
|
errorDiv.remove();
|
|
}
|
|
})
|
|
.catch(err => console.error('Error fetching status:', err));
|
|
}
|
|
});
|
|
}, 2000);
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
startStatusPolling();
|
|
|
|
const form = document.getElementById('uploadForm');
|
|
if (form) {
|
|
form.addEventListener('submit', (e) => {
|
|
if (form.dataset.submitting === 'true') {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
form.dataset.submitting = 'true';
|
|
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
if (submitBtn) {
|
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>{% translate "Téléversement en cours..." %}';
|
|
setTimeout(() => {
|
|
submitBtn.disabled = true;
|
|
}, 50);
|
|
}
|
|
|
|
let loadingText = document.getElementById('uploadLoadingText');
|
|
if (!loadingText) {
|
|
loadingText = document.createElement('div');
|
|
loadingText.id = 'uploadLoadingText';
|
|
loadingText.className = 'alert alert-info mt-3 small d-flex align-items-center gap-2';
|
|
loadingText.style.backgroundColor = 'rgba(13, 148, 136, 0.15)';
|
|
loadingText.style.borderColor = 'rgba(13, 148, 136, 0.3)';
|
|
loadingText.style.color = '#2dd4bf';
|
|
loadingText.innerHTML = `
|
|
<div class="spinner-grow spinner-grow-sm" role="status" style="color: #2dd4bf; flex-shrink: 0;"></div>
|
|
<div><strong>{% translate "Téléversement des fichiers volumineux en cours..." %}</strong><br>{% translate "Veuillez ne pas quitter cette page ni fermer le navigateur pendant le chargement." %}</div>
|
|
`;
|
|
form.appendChild(loadingText);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
</div>
|
|
</div>
|
|
{% endblock main %}
|