- Route POST /rename : renomme un fichier CDN avec validation sécurité,
retourne JSON (name, path, browse_url)
- Route /resize : accepte param `conflict` (backup | overwrite | rename | skip)
backup → renomme l'existant en {stem}_bak_{timestamp}{ext} avant création
rename → auto-incrémente le nom de la copie ({stem}_1, _2…)
overwrite → écrase silencieusement
skip → ignore (signalé dans les erreurs)
- browse.html : bouton ✏️ par fichier, renommage inline avec Entrée/Échap
- preview_image.html : bouton ✏️ dans l'en-tête, champ inline + redirect
après validation ; radio segmenté pour la stratégie de conflit
- app.css : styles btn-rename, rename-inline, radio-chips segmentés
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
210 lines
7.5 KiB
HTML
210 lines
7.5 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}{{ filename }}{% endblock %}
|
||
|
||
{% block content %}
|
||
|
||
<section class="card">
|
||
<div class="preview-header">
|
||
<a href="{{ url_for('browse', subpath=parent_path) if parent_path else url_for('browse') }}"
|
||
class="back-link">← Retour</a>
|
||
<h1 id="preview-title">{{ filename }}</h1>
|
||
<button type="button" id="rename-toggle" class="btn-icon" title="Renommer ce fichier">✏️</button>
|
||
{% include '_preview_nav.html' %}
|
||
</div>
|
||
<div id="rename-inline" class="rename-inline" style="display:none">
|
||
<input type="text" id="rename-input" class="rename-input" value="{{ filename }}">
|
||
<button type="button" id="rename-save" class="btn-rename-ok" title="Valider">✓</button>
|
||
<button type="button" id="rename-cancel" class="btn-rename-cancel" title="Annuler">✕</button>
|
||
<span id="rename-error" class="rename-error"></span>
|
||
</div>
|
||
<div class="preview-meta">
|
||
<span>Taille : <strong>{{ filesize }}</strong></span>
|
||
<span>Modifié : <strong>{{ mtime.strftime('%d/%m/%Y %H:%M') }}</strong></span>
|
||
<a href="{{ raw_url }}" download="{{ filename }}" class="btn btn-primary" style="margin-left:auto">
|
||
Télécharger
|
||
</a>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="preview-image-wrap">
|
||
<img src="{{ raw_url }}" alt="{{ filename }}">
|
||
</div>
|
||
|
||
<section class="card resize-card">
|
||
<h2>Créer des copies redimensionnées</h2>
|
||
<div class="resize-body">
|
||
|
||
<div class="resize-group">
|
||
<div class="resize-group-label">Tailles (px)</div>
|
||
<div class="resize-chips">
|
||
{% for size in [32, 64, 100, 128, 200, 300, 500, 600, 1024] %}
|
||
<label class="chip">
|
||
<input type="checkbox" class="resize-sz" value="{{ size }}">
|
||
<span>{{ size }}×{{ size }}</span>
|
||
</label>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="resize-group">
|
||
<div class="resize-group-label">Formats</div>
|
||
<div class="resize-chips">
|
||
{% for fmt in ['png', 'jpg', 'ico'] %}
|
||
<label class="chip">
|
||
<input type="checkbox" class="resize-fmt" value="{{ fmt }}">
|
||
<span>.{{ fmt }}</span>
|
||
</label>
|
||
{% endfor %}
|
||
<label class="chip {% if ext != '.svg' %}chip--disabled{% endif %}"
|
||
title="{% if ext != '.svg' %}SVG uniquement disponible si la source est SVG{% endif %}">
|
||
<input type="checkbox" class="resize-fmt" value="svg"
|
||
{% if ext != '.svg' %}disabled{% endif %}>
|
||
<span>.svg</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="resize-group">
|
||
<div class="resize-group-label">Si le fichier existe déjà</div>
|
||
<div class="resize-chips resize-chips--radio">
|
||
<label class="chip">
|
||
<input type="radio" name="conflict" value="backup" checked>
|
||
<span>Backup</span>
|
||
</label>
|
||
<label class="chip">
|
||
<input type="radio" name="conflict" value="overwrite">
|
||
<span>Écraser</span>
|
||
</label>
|
||
<label class="chip">
|
||
<input type="radio" name="conflict" value="rename">
|
||
<span>Renommer la copie</span>
|
||
</label>
|
||
<label class="chip">
|
||
<input type="radio" name="conflict" value="skip">
|
||
<span>Ignorer</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="resize-actions">
|
||
<button id="resize-btn" class="btn btn-primary" disabled>Générer les copies</button>
|
||
<span class="resize-hint">Les fichiers sont créés dans le même dossier</span>
|
||
</div>
|
||
|
||
<div id="resize-result" class="resize-result" style="display:none"></div>
|
||
|
||
</div>
|
||
</section>
|
||
|
||
<script>
|
||
(function () {
|
||
/* ── Rename ── */
|
||
const RENAME_URL = {{ url_for('rename_file') | tojson }};
|
||
const FILE_PATH = {{ subpath | tojson }};
|
||
|
||
const renameToggle = document.getElementById('rename-toggle');
|
||
const renameInline = document.getElementById('rename-inline');
|
||
const renameInput = document.getElementById('rename-input');
|
||
const renameSave = document.getElementById('rename-save');
|
||
const renameCancel = document.getElementById('rename-cancel');
|
||
const renameError = document.getElementById('rename-error');
|
||
|
||
renameToggle.addEventListener('click', () => {
|
||
const open = renameInline.style.display !== 'none';
|
||
renameInline.style.display = open ? 'none' : 'flex';
|
||
if (!open) { renameInput.focus(); renameInput.select(); }
|
||
renameError.textContent = '';
|
||
});
|
||
|
||
renameCancel.addEventListener('click', () => {
|
||
renameInline.style.display = 'none';
|
||
renameError.textContent = '';
|
||
});
|
||
|
||
async function doRename() {
|
||
const newName = renameInput.value.trim();
|
||
if (!newName) return;
|
||
renameSave.disabled = true;
|
||
const fd = new FormData();
|
||
fd.append('path', FILE_PATH);
|
||
fd.append('new_name', newName);
|
||
try {
|
||
const resp = await fetch(RENAME_URL, { method: 'POST', body: fd });
|
||
const data = await resp.json();
|
||
if (data.error) { renameError.textContent = data.error; return; }
|
||
window.location.href = data.browse_url;
|
||
} catch (_) {
|
||
renameError.textContent = 'Erreur réseau.';
|
||
} finally {
|
||
renameSave.disabled = false;
|
||
}
|
||
}
|
||
|
||
renameSave.addEventListener('click', doRename);
|
||
renameInput.addEventListener('keydown', e => {
|
||
if (e.key === 'Enter') doRename();
|
||
if (e.key === 'Escape') renameCancel.click();
|
||
});
|
||
|
||
/* ── Resize ── */
|
||
const szCbs = document.querySelectorAll('.resize-sz');
|
||
const fmtCbs = document.querySelectorAll('.resize-fmt');
|
||
const btn = document.getElementById('resize-btn');
|
||
const result = document.getElementById('resize-result');
|
||
const RESIZE_URL = {{ url_for('resize_image') | tojson }};
|
||
|
||
function canSubmit() {
|
||
return Array.from(szCbs).some(c => c.checked)
|
||
&& Array.from(fmtCbs).some(c => c.checked);
|
||
}
|
||
[...szCbs, ...fmtCbs].forEach(c => c.addEventListener('change', () => {
|
||
btn.disabled = !canSubmit();
|
||
}));
|
||
|
||
btn.addEventListener('click', async () => {
|
||
const conflict = document.querySelector('input[name="conflict"]:checked')?.value || 'skip';
|
||
const fd = new FormData();
|
||
fd.append('path', FILE_PATH);
|
||
fd.append('conflict', conflict);
|
||
szCbs.forEach(c => { if (c.checked) fd.append('sizes', c.value); });
|
||
fmtCbs.forEach(c => { if (c.checked) fd.append('formats', c.value); });
|
||
|
||
btn.disabled = true;
|
||
btn.textContent = 'Génération en cours…';
|
||
result.style.display = 'none';
|
||
|
||
try {
|
||
const resp = await fetch(RESIZE_URL, { method: 'POST', body: fd });
|
||
const data = await resp.json();
|
||
let html = '';
|
||
|
||
if (data.created && data.created.length) {
|
||
html += '<p class="resize-ok-title">✓ ' + data.created.length + ' fichier(s) créé(s)</p>';
|
||
html += '<ul class="resize-list">';
|
||
data.created.forEach(f => {
|
||
html += '<li><a href="/browse/' + f.path + '">' + f.name + '</a></li>';
|
||
});
|
||
html += '</ul>';
|
||
}
|
||
if (data.errors && data.errors.length) {
|
||
html += '<p class="resize-err-title">⚠ ' + data.errors.length + ' erreur(s)</p>';
|
||
html += '<ul class="resize-list resize-list--err">';
|
||
data.errors.forEach(e => {
|
||
html += '<li><code>' + (e.name || '') + '</code> : ' + e.reason + '</li>';
|
||
});
|
||
html += '</ul>';
|
||
}
|
||
if (!html) html = '<p class="resize-none">Aucun fichier généré.</p>';
|
||
result.innerHTML = html;
|
||
} catch (_) {
|
||
result.innerHTML = '<p class="resize-err-title">Erreur réseau.</p>';
|
||
}
|
||
|
||
result.style.display = 'block';
|
||
btn.textContent = 'Générer les copies';
|
||
btn.disabled = !canSubmit();
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
{% endblock %}
|