- 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>
299 lines
11 KiB
HTML
299 lines
11 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>
|
||
|
||
{% if meta %}
|
||
<section class="card meta-card">
|
||
<div class="meta-grid">
|
||
{% if meta.width and meta.height %}
|
||
<div class="meta-item">
|
||
<span class="meta-label">Dimensions</span>
|
||
<span class="meta-value">{{ meta.width }} × {{ meta.height }} px</span>
|
||
</div>
|
||
{% endif %}
|
||
{% if meta.format %}
|
||
<div class="meta-item">
|
||
<span class="meta-label">Format</span>
|
||
<span class="meta-value">{{ meta.format }}</span>
|
||
</div>
|
||
{% endif %}
|
||
{% if meta.mode %}
|
||
<div class="meta-item">
|
||
<span class="meta-label">Mode couleur</span>
|
||
<span class="meta-value">{{ meta.mode }}</span>
|
||
</div>
|
||
{% endif %}
|
||
{% if meta.dpi %}
|
||
<div class="meta-item">
|
||
<span class="meta-label">Résolution</span>
|
||
<span class="meta-value">{{ meta.dpi[0]|int }} × {{ meta.dpi[1]|int }} DPI</span>
|
||
</div>
|
||
{% endif %}
|
||
{% if meta.exif %}
|
||
{% set labels = {
|
||
'Make':'Appareil', 'Model':'Modèle',
|
||
'Software':'Logiciel', 'DateTime':'Modifié le',
|
||
'DateTimeOriginal':'Pris le','DateTimeDigitized':'Numérisé le',
|
||
'ExposureTime':'Exposition','FNumber':'Ouverture',
|
||
'ISOSpeedRatings':'ISO', 'FocalLength':'Focale',
|
||
'Flash':'Flash', 'WhiteBalance':'Balance blancs',
|
||
'ExposureProgram':'Programme','MeteringMode':'Mesure',
|
||
'Orientation':'Orientation','Artist':'Auteur',
|
||
'Copyright':'Copyright', 'ColorSpace':'Espace colorimétrique',
|
||
'GPS':'Coordonnées GPS',
|
||
} %}
|
||
{% for key, val in meta.exif.items() %}
|
||
<div class="meta-item">
|
||
<span class="meta-label">{{ labels.get(key, key) }}</span>
|
||
<span class="meta-value">
|
||
{% if key == 'GPS' and val %}
|
||
<a href="https://www.openstreetmap.org/?mlat={{ val.split(',')[0].strip() }}&mlon={{ val.split(',')[1].strip() }}&zoom=15"
|
||
target="_blank" rel="noopener">{{ val }}</a>
|
||
{% else %}
|
||
{{ val }}
|
||
{% endif %}
|
||
</span>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
</div>
|
||
</section>
|
||
{% endif %}
|
||
|
||
<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-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>
|
||
</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 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)
|
||
&& Array.from(fmtCbs).some(c => c.checked);
|
||
}
|
||
[...szCbs, ...fmtCbs].forEach(c => c.addEventListener('change', () => {
|
||
btn.disabled = !canSubmit();
|
||
conflictPanel.style.display = 'none';
|
||
resizeResolved = false;
|
||
}));
|
||
|
||
async function doResize(conflict) {
|
||
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();
|
||
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>
|
||
|
||
{% endblock %}
|