feat: conflit resize à la demande + nommage sans suffixe dimensions
- Route POST /check-resize : pre-check des fichiers cibles avant génération (utilise déjà le strip _NxN sur le stem) - Route /resize : strip du suffixe _NxN dans le stem source (logo_1024x1024.png → 500x500 = logo_500x500.png) - preview_image.html : bloc conflit masqué par défaut Au clic Générer → pre-check AJAX → si conflit : panneau jaune identique à l'upload avec Backup/Écraser/Renommer/Ignorer puis Confirmer Sans conflit → génération directe sans interruption Ferme #10. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
31ddff2a75
commit
8f6aa292ef
2 changed files with 102 additions and 32 deletions
41
app/app.py
41
app/app.py
|
|
@ -1,6 +1,7 @@
|
|||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
|
|
@ -663,6 +664,44 @@ def upload_file():
|
|||
return redirect(url_for("browse", subpath=subpath) if subpath else url_for("browse"))
|
||||
|
||||
|
||||
# ── Vérification des conflits avant redimensionnement ────────────────
|
||||
|
||||
@app.route("/check-resize", methods=["POST"])
|
||||
def check_resize():
|
||||
redir = _require_admin()
|
||||
if redir:
|
||||
return redir
|
||||
|
||||
subpath = request.form.get("path", "").strip()
|
||||
raw_sizes = request.form.getlist("sizes")
|
||||
raw_formats = request.form.getlist("formats")
|
||||
|
||||
if not subpath:
|
||||
return jsonify({"conflicts": []})
|
||||
|
||||
target = _safe_path(subpath)
|
||||
if not target.is_file():
|
||||
return jsonify({"conflicts": []})
|
||||
|
||||
try:
|
||||
sizes = [int(s) for s in raw_sizes if int(s) in RESIZE_SIZES]
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({"conflicts": []})
|
||||
|
||||
formats = [f for f in raw_formats if f in RESIZE_FORMATS]
|
||||
stem = re.sub(r"_\d+x\d+$", "", target.stem)
|
||||
parent = target.parent
|
||||
|
||||
conflicts = []
|
||||
for fmt in formats:
|
||||
for size in sizes:
|
||||
out_name = f"{stem}_{size}x{size}.{fmt}"
|
||||
if (parent / out_name).exists():
|
||||
conflicts.append(out_name)
|
||||
|
||||
return jsonify({"conflicts": conflicts})
|
||||
|
||||
|
||||
# ── Redimensionnement d'images ────────────────────────────────────────
|
||||
|
||||
@app.route("/resize", methods=["POST"])
|
||||
|
|
@ -704,7 +743,7 @@ def resize_image():
|
|||
conflict = "skip"
|
||||
|
||||
is_svg = ext == ".svg"
|
||||
stem = target.stem
|
||||
stem = re.sub(r"_\d+x\d+$", "", target.stem)
|
||||
parent = target.parent
|
||||
created, errors = [], []
|
||||
|
||||
|
|
|
|||
|
|
@ -122,33 +122,25 @@
|
|||
</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-conflict-panel" class="conflict-panel" style="display:none">
|
||||
<p class="conflict-title">⚠ Ces fichiers existent déjà : <strong id="resize-conflict-list"></strong></p>
|
||||
<div class="resize-chips resize-chips--radio">
|
||||
<label class="chip"><input type="radio" name="resize-conflict" value="backup" checked><span>Backup</span></label>
|
||||
<label class="chip"><input type="radio" name="resize-conflict" value="overwrite"><span>Écraser</span></label>
|
||||
<label class="chip"><input type="radio" name="resize-conflict" value="rename"><span>Renommer</span></label>
|
||||
<label class="chip"><input type="radio" name="resize-conflict" value="skip"><span>Ignorer</span></label>
|
||||
</div>
|
||||
<div class="conflict-actions">
|
||||
<button type="button" id="resize-confirm" class="btn btn-primary">Confirmer la génération</button>
|
||||
<button type="button" id="resize-cancel-conflict" class="btn-rename-cancel">Annuler</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="resize-result" class="resize-result" style="display:none"></div>
|
||||
|
||||
</div>
|
||||
|
|
@ -209,7 +201,13 @@
|
|||
const fmtCbs = document.querySelectorAll('.resize-fmt');
|
||||
const btn = document.getElementById('resize-btn');
|
||||
const result = document.getElementById('resize-result');
|
||||
const conflictPanel = document.getElementById('resize-conflict-panel');
|
||||
const conflictList = document.getElementById('resize-conflict-list');
|
||||
const confirmBtn = document.getElementById('resize-confirm');
|
||||
const cancelConflict= document.getElementById('resize-cancel-conflict');
|
||||
const RESIZE_URL = {{ url_for('resize_image') | tojson }};
|
||||
const CHECK_RESIZE_URL= {{ url_for('check_resize') | tojson }};
|
||||
let resizeResolved = false;
|
||||
|
||||
function canSubmit() {
|
||||
return Array.from(szCbs).some(c => c.checked)
|
||||
|
|
@ -217,10 +215,11 @@
|
|||
}
|
||||
[...szCbs, ...fmtCbs].forEach(c => c.addEventListener('change', () => {
|
||||
btn.disabled = !canSubmit();
|
||||
conflictPanel.style.display = 'none';
|
||||
resizeResolved = false;
|
||||
}));
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
const conflict = document.querySelector('input[name="conflict"]:checked')?.value || 'skip';
|
||||
async function doResize(conflict) {
|
||||
const fd = new FormData();
|
||||
fd.append('path', FILE_PATH);
|
||||
fd.append('conflict', conflict);
|
||||
|
|
@ -235,7 +234,6 @@
|
|||
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">';
|
||||
|
|
@ -257,10 +255,43 @@
|
|||
} 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();
|
||||
resizeResolved = false;
|
||||
}
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
const fd = new FormData();
|
||||
fd.append('path', FILE_PATH);
|
||||
szCbs.forEach(c => { if (c.checked) fd.append('sizes', c.value); });
|
||||
fmtCbs.forEach(c => { if (c.checked) fd.append('formats', c.value); });
|
||||
|
||||
let conflicts = [];
|
||||
try {
|
||||
const resp = await fetch(CHECK_RESIZE_URL, { method: 'POST', body: fd });
|
||||
const data = await resp.json();
|
||||
conflicts = data.conflicts || [];
|
||||
} catch (_) { /* réseau : on génère directement */ }
|
||||
|
||||
if (!conflicts.length) {
|
||||
doResize('overwrite');
|
||||
return;
|
||||
}
|
||||
|
||||
conflictList.textContent = conflicts.join(', ');
|
||||
conflictPanel.style.display = 'block';
|
||||
conflictPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
});
|
||||
|
||||
confirmBtn.addEventListener('click', () => {
|
||||
const strategy = document.querySelector('input[name="resize-conflict"]:checked')?.value || 'skip';
|
||||
conflictPanel.style.display = 'none';
|
||||
doResize(strategy);
|
||||
});
|
||||
|
||||
cancelConflict.addEventListener('click', () => {
|
||||
conflictPanel.style.display = 'none';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue