alpinux-static/app/templates/preview_image.html
Alpinux 14259c59f1 feat: renommage de fichiers + gestion des conflits dans le redimensionnement
- 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>
2026-05-06 09:26:26 +02:00

210 lines
7.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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 %}