fix: mobile menu styling + nextcloud search/mtime/bambu hint
Deploy via Portainer / deploy (push) Successful in 1s

This commit is contained in:
Jo
2026-07-07 11:13:10 +02:00
parent 10fdeb3ebc
commit 4206a8934e
3 changed files with 89 additions and 15 deletions
+8 -1
View File
@@ -1970,7 +1970,14 @@ def _parse_propfind(xml_bytes, base_url, username, current_path):
items.append({'name': name, 'path': rel, 'is_dir': is_dir,
'size': size, 'mtime': mtime, 'ext': ext if not is_dir else 'dir'})
items.sort(key=lambda x: (0 if x['is_dir'] else 1, x['name'].lower()))
def _mtime_ts(m):
try:
from email.utils import parsedate_to_datetime
return parsedate_to_datetime(m).timestamp()
except Exception:
return 0
items.sort(key=lambda x: (0 if x['is_dir'] else 1, -_mtime_ts(x['mtime'])))
return items
@app.route('/api/nextcloud/browse')
+28
View File
@@ -106,6 +106,34 @@
.stock-low { color: #f59e0b; }
.stock-empty { color: #ef4444; }
/* ── Mobile offcanvas nav — mêmes styles que #sidebar nav ──────────── */
#mobileSidebar nav { flex: 1; padding: .5rem 0; }
#mobileSidebar nav a {
display: flex; align-items: center; gap: .65rem;
padding: .6rem 1.25rem; color: #9ba3af;
text-decoration: none; font-size: .875rem;
transition: background .15s, color .15s;
white-space: nowrap;
}
#mobileSidebar nav a:hover,
#mobileSidebar nav a.active { background: #2e323d; color: #fff; }
#mobileSidebar nav a.active { border-left: 3px solid #ff6b35; }
#mobileSidebar nav a i { flex-shrink: 0; font-size: 1rem; width: 1.1rem; text-align: center; }
#mobileSidebar nav .nav-label { opacity: 1 !important; pointer-events: auto !important; }
#mobileSidebar nav .nav-section {
padding: .75rem 1.25rem .25rem; font-size: .7rem;
text-transform: uppercase; letter-spacing: .08em; color: #4b5563;
opacity: 1 !important;
}
#mobileSidebar .sidebar-user {
padding: .75rem 1.25rem; border-top: 1px solid #2e323d;
opacity: 1 !important; pointer-events: auto !important;
}
#mobileSidebar .sidebar-user div { font-size: .75rem; color: #9ba3af; }
#mobileSidebar .sidebar-user a {
font-size: .72rem; color: #6b7280; text-decoration: none; display: block; margin-top: .25rem;
}
/* ── Mobile responsive ───────────────────────────────────────────────── */
@media (max-width: 767.98px) {
#sidebar { display: none !important; }
+53 -14
View File
@@ -317,16 +317,22 @@
<div class="modal fade" id="ncPickerModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<div class="modal-header pb-2">
<div class="flex-fill">
<h5 class="modal-title mb-1"><i class="bi bi-cloud me-2"></i>Nextcloud — Choisir un fichier</h5>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0 small" id="ncBreadcrumb"></ol>
<ol class="breadcrumb mb-2 small" id="ncBreadcrumb"></ol>
</nav>
<input type="text" id="ncSearchInput" class="form-control form-control-sm"
placeholder="Filtrer par nom…" autocomplete="off">
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<button type="button" class="btn-close ms-2 align-self-start mt-1" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0" id="ncPickerBody" style="max-height:60vh;overflow-y:auto">
<div id="ncBambuHint" class="alert alert-info d-none m-2 mb-0 py-2 px-3 small" role="alert">
<i class="bi bi-info-circle me-1"></i>
Sélectionnez le fichier <code>.gcode.3mf</code> pour importer poids, durée et données de découpe.
</div>
<div class="modal-body p-0" id="ncPickerBody" style="max-height:55vh;overflow-y:auto">
<div class="text-center text-muted py-5">
<i class="bi bi-cloud fs-1 d-block mb-2"></i>
Cliquez sur le bouton <i class="bi bi-cloud-arrow-down"></i> pour ouvrir.
@@ -752,16 +758,20 @@ if (document.getElementById('weight_g').value) recalc();
// ── Nextcloud File Picker ─────────────────────────────────────────────────────
let ncPickerMode = 'source'; // 'source' = remplit seulement sourceFile, 'bambu' = remplit + parse 3MF
(function () {
const modal = document.getElementById('ncPickerModal');
const body = document.getElementById('ncPickerBody');
const bread = document.getElementById('ncBreadcrumb');
const sfInput = document.getElementById('sourceFile');
const modal = document.getElementById('ncPickerModal');
const body = document.getElementById('ncPickerBody');
const bread = document.getElementById('ncBreadcrumb');
const sfInput = document.getElementById('sourceFile');
const searchInput = document.getElementById('ncSearchInput');
const bambuHint = document.getElementById('ncBambuHint');
if (!modal) return;
let currentPath = '';
let allItems = [];
async function browse(path) {
searchInput.value = '';
body.innerHTML = '<div class="text-center py-4"><div class="spinner-border spinner-border-sm"></div> Chargement…</div>';
try {
const r = await fetch('/api/nextcloud/browse?path=' + encodeURIComponent(path));
@@ -771,13 +781,21 @@ let ncPickerMode = 'source'; // 'source' = remplit seulement sourceFile, 'bambu'
return;
}
currentPath = d.path;
allItems = d.items;
renderBreadcrumb(d.breadcrumb);
renderItems(d.items);
renderItems(allItems);
} catch (e) {
body.innerHTML = `<div class="alert alert-danger m-3">${e.message}</div>`;
}
}
searchInput.addEventListener('input', () => {
const q = searchInput.value.trim().toLowerCase();
if (!q) { renderItems(allItems); return; }
const filtered = allItems.filter(it => it.name.toLowerCase().includes(q));
renderItems(filtered);
});
function renderBreadcrumb(crumbs) {
bread.innerHTML = crumbs.map((c, i) =>
i < crumbs.length - 1
@@ -798,9 +816,22 @@ let ncPickerMode = 'source'; // 'source' = remplit seulement sourceFile, 'bambu'
'dir': 'bi-folder-fill text-warning',
};
function fmtMtime(mtime) {
if (!mtime) return '';
try {
const d = new Date(mtime);
const now = new Date();
const diffMs = now - d;
if (diffMs < 86400000) return 'Aujourd\'hui';
if (diffMs < 172800000) return 'Hier';
if (diffMs < 604800000) return Math.floor(diffMs/86400000) + 'j';
return d.toLocaleDateString('fr-FR', {day:'2-digit', month:'2-digit', year:'2-digit'});
} catch (e) { return ''; }
}
function renderItems(items) {
if (!items.length) {
body.innerHTML = '<div class="text-center text-muted py-4">Dossier vide.</div>';
body.innerHTML = '<div class="text-center text-muted py-4">Aucun fichier trouvé.</div>';
return;
}
body.innerHTML = '';
@@ -808,12 +839,16 @@ let ncPickerMode = 'source'; // 'source' = remplit seulement sourceFile, 'bambu'
list.className = 'list-group list-group-flush';
items.forEach(item => {
const icon = EXT_ICONS[item.ext] || 'bi-file text-muted';
const size = item.is_dir ? '' : ` <small class="text-muted">(${(item.size/1024/1024).toFixed(1)} Mo)</small>`;
const meta = item.is_dir ? '' :
`<small class="text-muted ms-auto text-nowrap" style="font-size:.75rem">
${fmtMtime(item.mtime)}${item.size ? ' · ' + (item.size/1024/1024).toFixed(1) + ' Mo' : ''}
</small>`;
const el = document.createElement('button');
el.type = 'button';
el.className = 'list-group-item list-group-item-action d-flex align-items-center gap-2 py-2';
el.innerHTML = `<i class="bi ${icon} fs-5" style="flex-shrink:0"></i>
<span class="flex-fill text-start">${item.name}${size}</span>
<span class="flex-fill text-start">${item.name}</span>
${meta}
${item.is_dir ? '<i class="bi bi-chevron-right text-muted"></i>' : '<i class="bi bi-check2-circle text-success"></i>'}`;
if (item.is_dir) {
el.addEventListener('click', () => browse(item.path));
@@ -869,8 +904,12 @@ let ncPickerMode = 'source'; // 'source' = remplit seulement sourceFile, 'bambu'
ncPickerMode = 'source'; // reset
}
// Ouvrir = charger le dossier racine
modal.addEventListener('show.bs.modal', () => { if (!currentPath) browse(''); });
// Ouvrir = charger le dossier racine + afficher/masquer hint bambu
modal.addEventListener('show.bs.modal', () => {
bambuHint.classList.toggle('d-none', ncPickerMode !== 'bambu');
searchInput.value = '';
if (!currentPath) browse('');
});
})();
</script>
{% endblock %}