alpinux-static/app/templates/preview_image.html
Alpinux a6d7bc2c8a feat: v2.0.0 — 14 tickets implémentés
#53 résumé req/IPs par statut dans Erreurs 404
#54 titre onglet avec compteur non résolus
#2  Tout/Aucun dans resize (tailles + formats)
#7  backup filename affiché dans résultats resize
#8  flash message résumé après upload
#6  renommage inline sur preview_text + preview_other
#23 filtre + tri par nom/taille/date dans corbeille
#20 sélection multiple + batch restore/delete corbeille
#45 /sitemap.xml (assets publics)
#52 ignoreip fail2ban sync sur Ignorer/Retirer une IP
#1  cairosvg>=2.7 dans requirements.txt
#51 ignored_ips.json exclu du rsync --delete
#48 as_cache/ exclu du rsync --delete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 21:04:03 +02:00

402 lines
15 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">
{% if from_trash %}
<a href="{{ url_for('trash_list') }}" class="back-link">← Corbeille</a>
{% else %}
<a href="{{ url_for('browse', subpath=parent_path) if parent_path else url_for('browse') }}"
class="back-link">← Retour</a>
{% endif %}
<h1 id="preview-title">{{ filename }}</h1>
{% if not from_trash %}
<button type="button" id="rename-toggle" class="btn-icon" title="Renommer ce fichier">✏️</button>
{% include '_preview_nav.html' %}
{% endif %}
</div>
{% if not from_trash %}
<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>
{% endif %}
<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',
'XResolution':'Résolution X','YResolution':'Résolution Y',
'ResolutionUnit':'Unité résolution',
'GPS':'Coordonnées GPS',
} %}
{% set color_spaces = {1: 'sRGB', 2: 'Adobe RGB', 65535: 'Non calibré'} %}
{% set resolution_units = {1: 'Sans unité', 2: 'pouces', 3: 'cm'} %}
{% 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>
{% elif key == 'ColorSpace' %}
{{ color_spaces.get(val | int, val) }}
{% elif key == 'ResolutionUnit' %}
{{ resolution_units.get(val | int, val) }}
{% else %}
{{ val }}
{% endif %}
</span>
</div>
{% endfor %}
{% endif %}
</div>
</section>
{% endif %}
<div class="preview-image-wrap">
<img src="{{ raw_url }}" alt="{{ filename }}">
</div>
{% if not from_trash %}
<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)
<span class="resize-sel-btns"><a href="#" class="btn-sel" data-grp="sz" data-val="all">Tout</a> · <a href="#" class="btn-sel" data-grp="sz" data-val="none">Aucun</a></span>
</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
<span class="resize-sel-btns"><a href="#" class="btn-sel" data-grp="fmt" data-val="all">Tout</a> · <a href="#" class="btn-sel" data-grp="fmt" data-val="none">Aucun</a></span>
</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>
{% if meta and meta.width and meta.height %}
<div class="resize-group">
<div class="resize-group-label">Dimension libre
<span class="resize-hint">(max {{ meta.width }} × {{ meta.height }} px)</span>
</div>
<div class="resize-custom-row">
<input type="number" id="custom-w" class="custom-dim" min="1" max="{{ meta.width }}"
placeholder="largeur" step="1">
<span class="custom-dim-sep">×</span>
<input type="number" id="custom-h" class="custom-dim" min="1" max="{{ meta.height }}"
placeholder="hauteur" step="1">
<span class="custom-dim-unit">px</span>
<label class="custom-square-lock">
<input type="checkbox" id="custom-square"> carré
</label>
</div>
<span id="custom-error" class="rename-error"></span>
</div>
{% endif %}
<div class="resize-actions">
<button id="resize-btn" class="btn btn-primary">Générer la copie</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 }};
const customW = document.getElementById('custom-w');
const customH = document.getElementById('custom-h');
const customSq = document.getElementById('custom-square');
const customErr = document.getElementById('custom-error');
let resizeResolved = false;
/* Sync W↔H when "carré" is checked */
if (customW && customH && customSq) {
customW.addEventListener('input', () => {
if (customSq.checked) customH.value = customW.value;
updateBtn(); customErr.textContent = '';
});
customH.addEventListener('input', () => {
if (customSq.checked) customW.value = customH.value;
updateBtn(); customErr.textContent = '';
});
customSq.addEventListener('change', () => {
if (customSq.checked && customW.value) customH.value = customW.value;
updateBtn();
});
}
function getCustomDim() {
if (!customW || !customH) return null;
const w = parseInt(customW.value, 10);
const h = parseInt(customH.value, 10);
if (!w || !h || w < 1 || h < 1) return null;
const maxW = parseInt(customW.max, 10);
const maxH = parseInt(customH.max, 10);
if (maxW && w > maxW) return null;
if (maxH && h > maxH) return null;
return `${w}x${h}`;
}
function countCopies() {
const nSizes = Array.from(szCbs).filter(c => c.checked).length
+ (getCustomDim() ? 1 : 0)
|| 1; // 0 sélectionné → dimensions d'origine conservées
const nFmts = Array.from(fmtCbs).filter(c => c.checked).length || 1;
return nSizes * nFmts;
}
function btnLabel() {
const n = countCopies();
return n === 1 ? 'Générer la copie' : `Générer les ${n} copies`;
}
function updateBtn() {
btn.textContent = btnLabel();
conflictPanel.style.display = 'none';
resizeResolved = false;
}
[...szCbs, ...fmtCbs].forEach(c => c.addEventListener('change', updateBtn));
updateBtn();
document.querySelectorAll('.btn-sel').forEach(a => {
a.addEventListener('click', e => {
e.preventDefault();
const cbs = document.querySelectorAll(`.resize-${a.dataset.grp}`);
cbs.forEach(cb => { if (!cb.disabled) cb.checked = a.dataset.val === 'all'; });
updateBtn();
});
});
function appendDims(fd) {
szCbs.forEach(c => { if (c.checked) fd.append('sizes', c.value); });
fmtCbs.forEach(c => { if (c.checked) fd.append('formats', c.value); });
const cd = getCustomDim();
if (cd) fd.append('custom_sizes', cd);
}
async function doResize(conflict) {
const fd = new FormData();
fd.append('path', FILE_PATH);
fd.append('conflict', conflict);
appendDims(fd);
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 => {
let line = '<a href="/browse/' + escHtml(f.path) + '">' + escHtml(f.name) + '</a>';
if (f.backup) line += ' <span class="resize-backup-hint" title="' + escHtml(f.backup) + '">← backup</span>';
html += '<li>' + line + '</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 = btnLabel();
btn.disabled = false;
resizeResolved = false;
}
btn.addEventListener('click', async () => {
if (customErr) customErr.textContent = '';
const fd = new FormData();
fd.append('path', FILE_PATH);
appendDims(fd);
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>
{% endif %}{# from_trash #}
{% endblock %}