#53 résumé req/IPs par statut dans Erreurs 404 #54 titre onglet avec compteur non résolus #2 Tout/Aucun dans resize (tailles + formats) #7 backup filename affiché dans résultats resize #8 flash message résumé après upload #6 renommage inline sur preview_text + preview_other #23 filtre + tri par nom/taille/date dans corbeille #20 sélection multiple + batch restore/delete corbeille #45 /sitemap.xml (assets publics) #52 ignoreip fail2ban sync sur Ignorer/Retirer une IP #1 cairosvg>=2.7 dans requirements.txt #51 ignored_ips.json exclu du rsync --delete #48 as_cache/ exclu du rsync --delete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
198 lines
7.8 KiB
HTML
198 lines
7.8 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Corbeille{% endblock %}
|
|
|
|
{% block content %}
|
|
|
|
<section class="card">
|
|
<div class="trash-header">
|
|
<h2>Corbeille
|
|
{% if entries %}
|
|
<span class="trash-count-label">— {{ entries|length }} fichier{{ 's' if entries|length > 1 else '' }}</span>
|
|
{% endif %}
|
|
</h2>
|
|
{% if entries %}
|
|
<form method="post" action="{{ url_for('trash_empty') }}"
|
|
onsubmit="return confirm('Vider définitivement toute la corbeille ?')">
|
|
<button type="submit" class="btn btn-danger">Vider la corbeille</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
<p class="trash-info">Les fichiers sont supprimés définitivement après 30 jours.</p>
|
|
|
|
{% if entries %}
|
|
<div class="trash-toolbar">
|
|
<input type="search" id="trash-filter" class="trash-filter" placeholder="Filtrer par nom…" autocomplete="off">
|
|
<button type="button" class="trash-sort-btn active" data-sort="date-desc">Date ▼</button>
|
|
<button type="button" class="trash-sort-btn" data-sort="name-asc">Nom ↑</button>
|
|
<button type="button" class="trash-sort-btn" data-sort="size-desc">Taille ▼</button>
|
|
</div>
|
|
<div id="trash-batch-bar" class="trash-batch-bar">
|
|
<span class="trash-select-count" id="batch-count">0 sélectionné(s)</span>
|
|
<button type="button" id="btn-restore-batch" class="btn btn-sm">♻️ Restaurer</button>
|
|
<button type="button" id="btn-delete-batch" class="btn btn-sm btn-danger">🗑 Supprimer</button>
|
|
</div>
|
|
{% set mode = 'trash' %}
|
|
{% include '_file_table.html' %}
|
|
{% else %}
|
|
<p class="empty">La corbeille est vide.</p>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<div id="restore-conflict-panel" class="conflict-panel" style="display:none">
|
|
<p class="conflict-title">⚠ Un fichier existe déjà à cet emplacement : <strong id="restore-conflict-name"></strong></p>
|
|
<div class="resize-chips resize-chips--radio">
|
|
<label class="chip"><input type="radio" name="restore-conflict" value="overwrite" checked><span>Écraser</span></label>
|
|
<label class="chip"><input type="radio" name="restore-conflict" value="rename"><span>Renommer</span></label>
|
|
</div>
|
|
<div class="conflict-actions">
|
|
<button type="button" id="restore-confirm" class="btn btn-primary">Confirmer la restauration</button>
|
|
<button type="button" id="restore-cancel" class="btn-rename-cancel">Annuler</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const RESTORE_URL = {{ url_for('trash_restore') | tojson }};
|
|
const RESTORE_BATCH_URL = {{ url_for('trash_restore_batch') | tojson }};
|
|
const DELETE_BATCH_URL = {{ url_for('trash_delete_batch') | tojson }};
|
|
const conflictPanel = document.getElementById('restore-conflict-panel');
|
|
const conflictName = document.getElementById('restore-conflict-name');
|
|
const confirmBtn = document.getElementById('restore-confirm');
|
|
const cancelBtn = document.getElementById('restore-cancel');
|
|
let pendingPath = null;
|
|
|
|
async function doRestore(path, conflict) {
|
|
const fd = new FormData();
|
|
fd.append('path', path);
|
|
if (conflict) fd.append('conflict', conflict);
|
|
try {
|
|
const resp = await fetch(RESTORE_URL, { method: 'POST', body: fd });
|
|
if (resp.status === 409) {
|
|
pendingPath = path;
|
|
conflictName.textContent = path.split('/').pop();
|
|
conflictPanel.style.display = 'block';
|
|
conflictPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
return;
|
|
}
|
|
if (resp.redirected) { window.location.href = resp.url; return; }
|
|
if (resp.ok) { window.location.href = resp.url || window.location.href; return; }
|
|
alert('Erreur lors de la restauration.');
|
|
} catch (_) {
|
|
alert('Erreur réseau.');
|
|
}
|
|
}
|
|
|
|
document.addEventListener('click', e => {
|
|
const btn = e.target.closest('.btn-restore');
|
|
if (!btn) return;
|
|
doRestore(btn.dataset.path, '');
|
|
});
|
|
|
|
confirmBtn.addEventListener('click', () => {
|
|
const strategy = document.querySelector('input[name="restore-conflict"]:checked')?.value || 'overwrite';
|
|
conflictPanel.style.display = 'none';
|
|
if (pendingPath) doRestore(pendingPath, strategy);
|
|
pendingPath = null;
|
|
});
|
|
|
|
cancelBtn.addEventListener('click', () => {
|
|
conflictPanel.style.display = 'none';
|
|
pendingPath = null;
|
|
});
|
|
|
|
/* ── Filtre ── */
|
|
const filterIn = document.getElementById('trash-filter');
|
|
const tbody = document.querySelector('#file-table tbody');
|
|
filterIn?.addEventListener('input', () => {
|
|
const q = filterIn.value.trim().toLowerCase();
|
|
tbody?.querySelectorAll('tr').forEach(row => {
|
|
row.style.display = (!q || (row.dataset.name || '').includes(q)) ? '' : 'none';
|
|
});
|
|
updateBatchBar();
|
|
});
|
|
|
|
/* ── Tri ── */
|
|
let currentSort = 'date-desc';
|
|
document.querySelectorAll('.trash-sort-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.trash-sort-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
currentSort = btn.dataset.sort;
|
|
sortTable(currentSort);
|
|
});
|
|
});
|
|
|
|
function sortTable(key) {
|
|
if (!tbody) return;
|
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
rows.sort((a, b) => {
|
|
if (key === 'name-asc') return (a.dataset.name || '').localeCompare(b.dataset.name || '');
|
|
if (key === 'size-desc') return parseInt(b.dataset.size || 0, 10) - parseInt(a.dataset.size || 0, 10);
|
|
/* date-desc */ return (b.dataset.date || '').localeCompare(a.dataset.date || '');
|
|
});
|
|
rows.forEach(r => tbody.appendChild(r));
|
|
}
|
|
|
|
/* ── Sélection multiple ── */
|
|
const selectAll = document.getElementById('select-all-cb');
|
|
const batchBar = document.getElementById('trash-batch-bar');
|
|
const batchCount = document.getElementById('batch-count');
|
|
|
|
function selectedCbs() {
|
|
return Array.from(document.querySelectorAll('.row-cb:checked'));
|
|
}
|
|
|
|
function updateBatchBar() {
|
|
const sel = selectedCbs().length;
|
|
if (batchBar) batchBar.classList.toggle('visible', sel > 0);
|
|
if (batchCount) batchCount.textContent = `${sel} sélectionné(s)`;
|
|
if (selectAll) {
|
|
const all = document.querySelectorAll('.row-cb:not(:disabled)').length;
|
|
selectAll.indeterminate = sel > 0 && sel < all;
|
|
selectAll.checked = sel > 0 && sel === all;
|
|
}
|
|
}
|
|
|
|
selectAll?.addEventListener('change', () => {
|
|
document.querySelectorAll('.row-cb').forEach(cb => {
|
|
const row = cb.closest('tr');
|
|
if (!row || row.style.display !== 'none') cb.checked = selectAll.checked;
|
|
});
|
|
updateBatchBar();
|
|
});
|
|
|
|
document.addEventListener('change', e => {
|
|
if (e.target.classList.contains('row-cb')) updateBatchBar();
|
|
});
|
|
|
|
/* ── Restaurer la sélection ── */
|
|
document.getElementById('btn-restore-batch')?.addEventListener('click', async () => {
|
|
const paths = selectedCbs().map(cb => cb.dataset.path);
|
|
if (!paths.length) return;
|
|
const fd = new FormData();
|
|
paths.forEach(p => fd.append('paths', p));
|
|
try {
|
|
const j = await fetch(RESTORE_BATCH_URL, { method: 'POST', body: fd }).then(r => r.json());
|
|
const msg = `${j.ok || 0} fichier(s) restauré(s)` + (j.conflict?.length ? `, ${j.conflict.length} conflit(s) ignoré(s)` : '') + (j.error ? `, ${j.error} erreur(s)` : '');
|
|
alert(msg);
|
|
location.reload();
|
|
} catch { alert('Erreur réseau.'); }
|
|
});
|
|
|
|
/* ── Supprimer la sélection ── */
|
|
document.getElementById('btn-delete-batch')?.addEventListener('click', async () => {
|
|
const cbs = selectedCbs();
|
|
if (!cbs.length) return;
|
|
const names = cbs.map(cb => cb.dataset.name).join(', ');
|
|
if (!confirm(`Supprimer définitivement ${cbs.length} fichier(s) ?\n${names}`)) return;
|
|
const fd = new FormData();
|
|
cbs.forEach(cb => fd.append('paths', cb.dataset.path));
|
|
try {
|
|
await fetch(DELETE_BATCH_URL, { method: 'POST', body: fd });
|
|
location.reload();
|
|
} catch { alert('Erreur réseau.'); }
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
{% endblock %}
|