Ajoute un sélecteur de stratégie dans le formulaire de dépôt CDN,
identique à celui du redimensionnement :
- Écraser (défaut) : comportement précédent, écrase silencieusement
- Backup : renomme l'existant en {stem}_bak_{timestamp}{ext} avant dépôt
- Renommer : auto-incrémente le nom du fichier uploadé ({stem}_1, _2…)
- Ignorer : ne dépose pas si le fichier existe déjà
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
211 lines
7.3 KiB
HTML
211 lines
7.3 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ breadcrumb[-1].name if breadcrumb else 'Parcourir' }}{% endblock %}
|
|
|
|
{% block content %}
|
|
|
|
<section class="card">
|
|
|
|
<nav class="breadcrumb">
|
|
<a href="{{ url_for('browse') }}">CDN</a>
|
|
{% for crumb in breadcrumb %}
|
|
<span class="sep">/</span>
|
|
{% if loop.last %}
|
|
<span class="current">{{ crumb.name }}</span>
|
|
{% else %}
|
|
<a href="{{ url_for('browse', subpath=crumb.path) }}">{{ crumb.name }}</a>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</nav>
|
|
|
|
{% if entries or subpath %}
|
|
<table class="file-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="col-icon"></th>
|
|
<th>Nom</th>
|
|
<th class="col-size">Taille</th>
|
|
<th class="col-date">Modifié le</th>
|
|
{% if has_hits %}<th class="col-hits">Vues</th>{% endif %}
|
|
<th class="col-actions"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
|
|
{% if subpath %}
|
|
<tr>
|
|
<td class="col-icon">⬆</td>
|
|
<td>
|
|
{% if breadcrumb | length > 1 %}
|
|
<a href="{{ url_for('browse', subpath=breadcrumb[-2].path) }}">.. (dossier parent)</a>
|
|
{% else %}
|
|
<a href="{{ url_for('browse') }}">.. (racine)</a>
|
|
{% endif %}
|
|
</td>
|
|
<td></td><td></td>
|
|
{% if has_hits %}<td></td>{% endif %}
|
|
<td></td>
|
|
</tr>
|
|
{% endif %}
|
|
|
|
{% for e in entries %}
|
|
<tr>
|
|
<td class="col-icon">
|
|
{%- if e.is_dir -%}📁
|
|
{%- elif e.is_image -%}🖼
|
|
{%- elif e.is_pdf -%}📕
|
|
{%- elif e.is_text -%}📄
|
|
{%- else -%}📎
|
|
{%- endif -%}
|
|
</td>
|
|
<td>
|
|
<div class="col-name">
|
|
{% if e.is_image %}
|
|
<img class="thumb-sm"
|
|
src="{{ url_for('raw_file', subpath=e.path) }}"
|
|
alt="{{ e.name }}"
|
|
loading="lazy">
|
|
{% endif %}
|
|
<a href="{{ url_for('browse', subpath=e.path) }}">
|
|
{{ e.name }}{% if e.is_dir %}/{% endif %}
|
|
</a>
|
|
{% if e.ext and not e.is_dir %}
|
|
<span class="type-badge">{{ e.ext }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
<td class="col-size">{{ humansize(e.size) if e.size is not none else '—' }}</td>
|
|
<td class="col-date">{{ e.mtime.strftime('%d/%m/%Y %H:%M') }}</td>
|
|
{% if has_hits %}
|
|
<td class="col-hits">
|
|
{% if not e.is_dir %}
|
|
{% if e.hits %}
|
|
<span class="hits-badge hits-active" title="{{ e.hits }} requête(s) dans les stats">{{ e.hits }}</span>
|
|
{% else %}
|
|
<span class="hits-badge hits-zero" title="Aucune vue dans les stats">0</span>
|
|
{% endif %}
|
|
{% endif %}
|
|
</td>
|
|
{% endif %}
|
|
<td class="col-actions">
|
|
{% if not e.is_dir %}
|
|
<div class="row-actions">
|
|
<button type="button" class="btn-rename"
|
|
data-path="{{ e.path }}" data-name="{{ e.name }}"
|
|
title="Renommer">✏️</button>
|
|
<form method="post" action="{{ url_for('delete_file') }}" style="display:contents">
|
|
<input type="hidden" name="path" value="{{ e.path }}">
|
|
<button type="submit" class="btn-del" title="Supprimer du CDN">🗑</button>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<p class="empty">Dossier vide.</p>
|
|
{% endif %}
|
|
|
|
</section>
|
|
|
|
<section class="card upload-card">
|
|
<h2>Déposer des fichiers dans {{ ('/' + subpath) if subpath else '/' }}</h2>
|
|
<form method="post" action="{{ url_for('upload_file') }}" enctype="multipart/form-data" id="upload-form">
|
|
<input type="hidden" name="path" value="{{ subpath }}">
|
|
<label class="drop-zone" for="upload-input">
|
|
<span class="drop-icon">📤</span>
|
|
<span class="drop-text">Glisser-déposer des fichiers ici<br>ou cliquer pour sélectionner</span>
|
|
<span class="drop-names" id="drop-names"></span>
|
|
<input type="file" name="files" id="upload-input" multiple>
|
|
</label>
|
|
<div class="upload-conflict">
|
|
<span class="resize-group-label">Si le fichier existe déjà</span>
|
|
<div class="resize-chips resize-chips--radio">
|
|
<label class="chip"><input type="radio" name="conflict" value="overwrite" checked><span>Écraser</span></label>
|
|
<label class="chip"><input type="radio" name="conflict" value="backup"><span>Backup</span></label>
|
|
<label class="chip"><input type="radio" name="conflict" value="rename"><span>Renommer</span></label>
|
|
<label class="chip"><input type="radio" name="conflict" value="skip"><span>Ignorer</span></label>
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary" id="upload-btn" disabled>Envoyer</button>
|
|
</form>
|
|
<script>
|
|
const inp = document.getElementById('upload-input');
|
|
const btn = document.getElementById('upload-btn');
|
|
const lbl = document.getElementById('drop-names');
|
|
inp.addEventListener('change', () => {
|
|
const n = inp.files.length;
|
|
lbl.textContent = n ? Array.from(inp.files).map(f => f.name).join(', ') : '';
|
|
btn.disabled = n === 0;
|
|
});
|
|
</script>
|
|
</section>
|
|
|
|
<script>
|
|
(function () {
|
|
function escHtml(s) {
|
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
async function doRename(path, newName, nameCell, origHtml) {
|
|
if (!newName || newName === path.split('/').pop()) {
|
|
nameCell.innerHTML = origHtml;
|
|
return;
|
|
}
|
|
const fd = new FormData();
|
|
fd.append('path', path);
|
|
fd.append('new_name', newName);
|
|
try {
|
|
const resp = await fetch('/rename', { method: 'POST', body: fd });
|
|
const data = await resp.json();
|
|
if (data.error) {
|
|
const inp = nameCell.querySelector('.rename-input');
|
|
if (inp) { inp.style.borderColor = '#b91c1c'; inp.title = data.error; }
|
|
return;
|
|
}
|
|
location.reload();
|
|
} catch (_) {
|
|
nameCell.innerHTML = origHtml;
|
|
}
|
|
}
|
|
|
|
document.addEventListener('click', e => {
|
|
const btn = e.target.closest('.btn-rename');
|
|
if (!btn) return;
|
|
|
|
const row = btn.closest('tr');
|
|
const nameCell = row.querySelector('.col-name');
|
|
const path = btn.dataset.path;
|
|
const name = btn.dataset.name;
|
|
const origHtml = nameCell.innerHTML;
|
|
|
|
const thumb = nameCell.querySelector('.thumb-sm');
|
|
const thumbHtml = thumb ? thumb.outerHTML : '';
|
|
|
|
nameCell.innerHTML =
|
|
thumbHtml +
|
|
'<input type="text" class="rename-input" value="' + escHtml(name) + '">' +
|
|
'<button class="btn-rename-ok" title="Valider">✓</button>' +
|
|
'<button class="btn-rename-cancel" title="Annuler">✕</button>' +
|
|
'<span class="rename-error"></span>';
|
|
|
|
const inp = nameCell.querySelector('.rename-input');
|
|
inp.focus(); inp.select();
|
|
|
|
nameCell.querySelector('.btn-rename-cancel').addEventListener('click', () => {
|
|
nameCell.innerHTML = origHtml;
|
|
});
|
|
nameCell.querySelector('.btn-rename-ok').addEventListener('click', () => {
|
|
doRename(path, inp.value.trim(), nameCell, origHtml);
|
|
});
|
|
inp.addEventListener('keydown', ev => {
|
|
if (ev.key === 'Enter') doRename(path, inp.value.trim(), nameCell, origHtml);
|
|
if (ev.key === 'Escape') nameCell.innerHTML = origHtml;
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
{% endblock %}
|