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 io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
|
|
@ -663,6 +664,44 @@ def upload_file():
|
||||||
return redirect(url_for("browse", subpath=subpath) if subpath else url_for("browse"))
|
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 ────────────────────────────────────────
|
# ── Redimensionnement d'images ────────────────────────────────────────
|
||||||
|
|
||||||
@app.route("/resize", methods=["POST"])
|
@app.route("/resize", methods=["POST"])
|
||||||
|
|
@ -704,7 +743,7 @@ def resize_image():
|
||||||
conflict = "skip"
|
conflict = "skip"
|
||||||
|
|
||||||
is_svg = ext == ".svg"
|
is_svg = ext == ".svg"
|
||||||
stem = target.stem
|
stem = re.sub(r"_\d+x\d+$", "", target.stem)
|
||||||
parent = target.parent
|
parent = target.parent
|
||||||
created, errors = [], []
|
created, errors = [], []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,33 +122,25 @@
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="resize-actions">
|
||||||
<button id="resize-btn" class="btn btn-primary" disabled>Générer les copies</button>
|
<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>
|
<span class="resize-hint">Les fichiers sont créés dans le même dossier</span>
|
||||||
</div>
|
</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 id="resize-result" class="resize-result" style="display:none"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -209,7 +201,13 @@
|
||||||
const fmtCbs = document.querySelectorAll('.resize-fmt');
|
const fmtCbs = document.querySelectorAll('.resize-fmt');
|
||||||
const btn = document.getElementById('resize-btn');
|
const btn = document.getElementById('resize-btn');
|
||||||
const result = document.getElementById('resize-result');
|
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 RESIZE_URL = {{ url_for('resize_image') | tojson }};
|
||||||
|
const CHECK_RESIZE_URL= {{ url_for('check_resize') | tojson }};
|
||||||
|
let resizeResolved = false;
|
||||||
|
|
||||||
function canSubmit() {
|
function canSubmit() {
|
||||||
return Array.from(szCbs).some(c => c.checked)
|
return Array.from(szCbs).some(c => c.checked)
|
||||||
|
|
@ -217,10 +215,11 @@
|
||||||
}
|
}
|
||||||
[...szCbs, ...fmtCbs].forEach(c => c.addEventListener('change', () => {
|
[...szCbs, ...fmtCbs].forEach(c => c.addEventListener('change', () => {
|
||||||
btn.disabled = !canSubmit();
|
btn.disabled = !canSubmit();
|
||||||
|
conflictPanel.style.display = 'none';
|
||||||
|
resizeResolved = false;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
btn.addEventListener('click', async () => {
|
async function doResize(conflict) {
|
||||||
const conflict = document.querySelector('input[name="conflict"]:checked')?.value || 'skip';
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('path', FILE_PATH);
|
fd.append('path', FILE_PATH);
|
||||||
fd.append('conflict', conflict);
|
fd.append('conflict', conflict);
|
||||||
|
|
@ -235,7 +234,6 @@
|
||||||
const resp = await fetch(RESIZE_URL, { method: 'POST', body: fd });
|
const resp = await fetch(RESIZE_URL, { method: 'POST', body: fd });
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
if (data.created && data.created.length) {
|
if (data.created && data.created.length) {
|
||||||
html += '<p class="resize-ok-title">✓ ' + data.created.length + ' fichier(s) créé(s)</p>';
|
html += '<p class="resize-ok-title">✓ ' + data.created.length + ' fichier(s) créé(s)</p>';
|
||||||
html += '<ul class="resize-list">';
|
html += '<ul class="resize-list">';
|
||||||
|
|
@ -257,10 +255,43 @@
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
result.innerHTML = '<p class="resize-err-title">Erreur réseau.</p>';
|
result.innerHTML = '<p class="resize-err-title">Erreur réseau.</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
result.style.display = 'block';
|
result.style.display = 'block';
|
||||||
btn.textContent = 'Générer les copies';
|
btn.textContent = 'Générer les copies';
|
||||||
btn.disabled = !canSubmit();
|
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>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue