alpinux-static/app/templates/browse.html
Alpinux 9857a92031 Parcourir : créer un dossier depuis le fil d'Ariane (v2.1.0)
Le bouton « 📁 Nouveau dossier » ouvre une saisie en ligne à droite du fil
d'Ariane (Entrée valide, Échap annule) et crée le sous-dossier dans le
répertoire affiché.

Endpoint POST /mkdir, réservé aux admins : nom nettoyé par secure_filename,
refus des noms réservés ou cachés (app, scripts, .git…), des traversées de
chemin, et des dossiers déjà existants (409).

Le README porte aussi les chemins locaux remis à jour après le déménagement
du dépôt dans ~/Projects/alpinux.static.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtgiPNG3inkbM8k7suMtjL
2026-09-20 08:39:05 +02:00

247 lines
8.7 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ breadcrumb[-1].name if breadcrumb else 'Parcourir' }}{% endblock %}
{% block content %}
<section class="card">
<div class="browse-head">
<nav class="breadcrumb">
<a href="{{ url_for('browse') }}">CDN</a>
{% for crumb in breadcrumb %}
<span class="sep">/</span>
{% if loop.last %}
<span class="current">{{ crumb.name }}</span>
{% else %}
<a href="{{ url_for('browse', subpath=crumb.path) }}">{{ crumb.name }}</a>
{% endif %}
{% endfor %}
</nav>
<div class="mkdir">
<button type="button" class="btn-sm" id="mkdir-open">📁 Nouveau dossier</button>
<div class="mkdir-form" id="mkdir-form" hidden>
<input type="text" class="rename-input" id="mkdir-input"
placeholder="Nom du dossier" autocomplete="off" maxlength="100">
<button type="button" class="btn-rename-ok" id="mkdir-ok" title="Créer">✓</button>
<button type="button" class="btn-rename-cancel" id="mkdir-cancel" title="Annuler">✕</button>
</div>
<span class="mkdir-error" id="mkdir-error"></span>
</div>
</div>
{% if entries or subpath %}
{% set mode = 'browse' %}
{% include '_file_table.html' %}
{% else %}
<p class="empty">Dossier vide.</p>
{% endif %}
</section>
<section class="card upload-card">
<h2>Déposer des fichiers dans {{ ('/' + subpath) if subpath else '/' }}</h2>
<form method="post" action="{{ url_for('upload_file') }}" enctype="multipart/form-data" id="upload-form">
<input type="hidden" name="path" value="{{ subpath }}">
<label class="drop-zone" for="upload-input">
<span class="drop-icon">📤</span>
<span class="drop-text">Glisser-déposer des fichiers ici<br>ou cliquer pour sélectionner</span>
<span class="drop-names" id="drop-names"></span>
<input type="file" name="files" id="upload-input" multiple>
</label>
<input type="hidden" name="conflict" id="conflict-value" value="overwrite">
<button type="submit" class="btn btn-primary" id="upload-btn" disabled>Envoyer</button>
<div id="upload-conflict-panel" class="conflict-panel" style="display:none">
<p class="conflict-title">⚠ Ces fichiers existent déjà : <strong id="conflict-list"></strong></p>
<div class="resize-chips resize-chips--radio">
<label class="chip"><input type="radio" name="conflict-choice" value="overwrite" checked><span>Écraser</span></label>
<label class="chip"><input type="radio" name="conflict-choice" value="backup"><span>Backup</span></label>
<label class="chip"><input type="radio" name="conflict-choice" value="rename"><span>Renommer</span></label>
<label class="chip"><input type="radio" name="conflict-choice" value="skip"><span>Ignorer</span></label>
</div>
<div class="conflict-actions">
<button type="button" id="conflict-confirm" class="btn btn-primary">Confirmer l'envoi</button>
<button type="button" id="conflict-cancel" class="btn-rename-cancel">Annuler</button>
</div>
</div>
</form>
<script>
(function () {
const form = document.getElementById('upload-form');
const inp = document.getElementById('upload-input');
const btn = document.getElementById('upload-btn');
const lbl = document.getElementById('drop-names');
const panel = document.getElementById('upload-conflict-panel');
const hidden = document.getElementById('conflict-value');
const listEl = document.getElementById('conflict-list');
const confirm = document.getElementById('conflict-confirm');
const cancel = document.getElementById('conflict-cancel');
const PATH = {{ subpath | tojson }};
let resolved = false;
inp.addEventListener('change', () => {
const n = inp.files.length;
lbl.textContent = n ? Array.from(inp.files).map(f => f.name).join(', ') : '';
btn.disabled = n === 0;
panel.style.display = 'none';
resolved = false;
});
form.addEventListener('submit', async (e) => {
if (resolved) return;
e.preventDefault();
const names = Array.from(inp.files).map(f => f.name);
let conflicts = [];
try {
const fd = new FormData();
fd.append('path', PATH);
names.forEach(n => fd.append('names', n));
const resp = await fetch({{ url_for('check_upload') | tojson }}, { method: 'POST', body: fd });
const data = await resp.json();
conflicts = data.conflicts || [];
} catch (_) { /* réseau : on laisse passer */ }
if (!conflicts.length) {
resolved = true;
form.submit();
return;
}
listEl.textContent = conflicts.join(', ');
panel.style.display = 'block';
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});
confirm.addEventListener('click', () => {
hidden.value = document.querySelector('input[name="conflict-choice"]:checked').value;
resolved = true;
form.submit();
});
cancel.addEventListener('click', () => {
panel.style.display = 'none';
});
})();
</script>
</section>
<script>
(function () {
const open = document.getElementById('mkdir-open');
const form = document.getElementById('mkdir-form');
const inp = document.getElementById('mkdir-input');
const err = document.getElementById('mkdir-error');
const PATH = {{ subpath | tojson }};
function close() {
form.hidden = true;
open.hidden = false;
err.textContent = '';
inp.value = '';
}
open.addEventListener('click', () => {
open.hidden = true;
form.hidden = false;
inp.focus();
});
document.getElementById('mkdir-cancel').addEventListener('click', close);
async function create() {
const name = inp.value.trim();
if (!name) { close(); return; }
err.textContent = '';
const fd = new FormData();
fd.append('path', PATH);
fd.append('name', name);
try {
const resp = await fetch({{ url_for('make_dir') | tojson }}, { method: 'POST', body: fd });
const data = await resp.json();
if (data.error) {
err.textContent = data.error;
inp.focus(); inp.select();
return;
}
location.reload();
} catch (_) {
err.textContent = 'Création impossible';
}
}
document.getElementById('mkdir-ok').addEventListener('click', create);
inp.addEventListener('keydown', ev => {
if (ev.key === 'Enter') { ev.preventDefault(); create(); }
if (ev.key === 'Escape') close();
});
})();
</script>
<script>
(function () {
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
async function doRename(path, newName, nameCell, origHtml) {
if (!newName || newName === path.split('/').pop()) {
nameCell.innerHTML = origHtml;
return;
}
const fd = new FormData();
fd.append('path', path);
fd.append('new_name', newName);
try {
const resp = await fetch('/rename', { method: 'POST', body: fd });
const data = await resp.json();
if (data.error) {
const inp = nameCell.querySelector('.rename-input');
if (inp) { inp.style.borderColor = '#b91c1c'; inp.title = data.error; }
return;
}
location.reload();
} catch (_) {
nameCell.innerHTML = origHtml;
}
}
document.addEventListener('click', e => {
const btn = e.target.closest('.btn-rename');
if (!btn) return;
const row = btn.closest('tr');
const nameCell = row.querySelector('.col-name');
const path = btn.dataset.path;
const name = btn.dataset.name;
const origHtml = nameCell.innerHTML;
const thumb = nameCell.querySelector('.thumb-sm');
const thumbHtml = thumb ? thumb.outerHTML : '';
nameCell.innerHTML =
thumbHtml +
'<input type="text" class="rename-input" value="' + escHtml(name) + '">' +
'<button class="btn-rename-ok" title="Valider">✓</button>' +
'<button class="btn-rename-cancel" title="Annuler">✕</button>' +
'<span class="rename-error"></span>';
const inp = nameCell.querySelector('.rename-input');
inp.focus(); inp.select();
nameCell.querySelector('.btn-rename-cancel').addEventListener('click', () => {
nameCell.innerHTML = origHtml;
});
nameCell.querySelector('.btn-rename-ok').addEventListener('click', () => {
doRename(path, inp.value.trim(), nameCell, origHtml);
});
inp.addEventListener('keydown', ev => {
if (ev.key === 'Enter') doRename(path, inp.value.trim(), nameCell, origHtml);
if (ev.key === 'Escape') nameCell.innerHTML = origHtml;
});
});
})();
</script>
{% endblock %}