// HouseHub — PrintVault module const API = '/modules/printvault/api.php'; const PV_BASE = 'https://printvault.nas.percolouco.com'; function escHtml(s) { const d = document.createElement('div'); d.textContent = String(s ?? ''); return d.innerHTML; } function fmtSize(b) { if (!b) return '—'; if (b > 1048576) return (b / 1048576).toFixed(1) + ' Mo'; return Math.round(b / 1024) + ' Ko'; } function fmtDate(s) { if (!s) return '—'; return new Date(s).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' }); } function toast(msg, type = 'success') { const c = document.getElementById('pv-toasts'); const t = document.createElement('div'); t.className = 'pv-toast' + (type === 'error' ? ' error' : ''); t.textContent = msg; c.appendChild(t); setTimeout(() => t.remove(), 3500); } async function api(action, method = 'GET', data = null, extra = '') { const opts = { method, credentials: 'same-origin', headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }; if (data && !(data instanceof FormData)) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(data); } else if (data instanceof FormData) { opts.body = data; } const r = await fetch(API + '?action=' + action + extra, opts); const text = await r.text(); let j; try { j = JSON.parse(text); } catch (e) { console.error('Bad JSON:', text.slice(0, 300)); throw new Error('Erreur serveur'); } if (!j.ok) throw new Error(j.error || 'Erreur'); return j.data; } // ─── State ──────────────────────────────────────────────────────────────────── let currentCategory = null; let currentType = null; let searchQuery = ''; let allModels = []; let categories = []; // ─── Init ───────────────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { loadSidebar(); loadModels(); document.getElementById('pv-search')?.addEventListener('input', e => { searchQuery = e.target.value.trim(); renderModels(); }); document.querySelectorAll('.pv-modal-backdrop').forEach(m => m.addEventListener('click', e => { if (e.target === m) m.classList.remove('show'); }) ); // Upload drop zone const dz = document.getElementById('pv-drop-zone'); if (dz) { dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('over'); }); dz.addEventListener('dragleave', () => dz.classList.remove('over')); dz.addEventListener('drop', e => { e.preventDefault(); dz.classList.remove('over'); const f = e.dataTransfer.files[0]; if (f) setUploadFile(f); }); dz.addEventListener('click', () => document.getElementById('pv-file-input').click()); document.getElementById('pv-file-input').addEventListener('change', e => { if (e.target.files[0]) setUploadFile(e.target.files[0]); }); } }); // ─── Sidebar ────────────────────────────────────────────────────────────────── async function loadSidebar() { try { const data = await api('categories'); categories = data.categories || []; renderSidebar(); } catch (e) { console.error(e); } } function renderSidebar() { const el = document.getElementById('pv-sidebar-nav'); let html = `
Catégories
🗂 Toutes ${allModels.length}
`; categories.forEach(c => { const count = allModels.filter(m => m.category === c.name).length; html += `
${escHtml(c.name)} ${count}
`; }); el.innerHTML = html; } function setCategory(cat) { currentCategory = cat; renderSidebar(); renderModels(); } function setType(type) { currentType = currentType === type ? null : type; document.querySelectorAll('.pv-chip').forEach(c => { c.classList.toggle('active', c.dataset.type === currentType); }); renderModels(); } // ─── Models ─────────────────────────────────────────────────────────────────── async function loadModels() { try { const data = await api('models'); allModels = data.models || []; renderSidebar(); renderModels(); updateHeader(); } catch (e) { toast(e.message, 'error'); } } function filterModels() { return allModels.filter(m => { if (currentCategory && m.category !== currentCategory) return false; if (currentType) { const ext = m.file_type.toLowerCase(); if (currentType === 'gcode' && !['gcode','gco','g'].includes(ext)) return false; if (currentType !== 'gcode' && ext !== currentType) return false; } if (searchQuery) { const q = searchQuery.toLowerCase(); if (!m.name.toLowerCase().includes(q) && !(m.description || '').toLowerCase().includes(q) && !(m.tags || '').toLowerCase().includes(q)) return false; } return true; }); } function renderModels() { const el = document.getElementById('pv-grid'); const filtered = filterModels(); if (!filtered.length) { el.innerHTML = `
🖨️

Aucun modèle trouvé.

`; updateHeader(0); return; } el.innerHTML = filtered.map(m => modelCardHtml(m)).join(''); updateHeader(filtered.length); } function modelCardHtml(m) { const ext = m.file_type.toLowerCase(); const typeIcon = { stl: '🟣', '3mf': '🔵', gcode: '🟢', g: '🟢', gco: '🟢' }[ext] || '📄'; const thumbSrc = m.thumb_filename ? `${PV_BASE}/thumbs/${m.thumb_filename}` : null; const thumbHtml = thumbSrc ? `` : typeIcon; const dims = m.dim_x > 0 ? `${Math.round(m.dim_x)}×${Math.round(m.dim_y)}×${Math.round(m.dim_z)} mm` : ''; const gcodeInfo = m.gcode_print_time ? m.gcode_print_time : ''; return `
${thumbHtml}
${escHtml(m.name)}
${escHtml(m.file_type.toUpperCase())} ${dims ? `${escHtml(dims)}` : ''} ${gcodeInfo ? `⏱ ${escHtml(gcodeInfo)}` : ''} ${escHtml(fmtSize(m.file_size))}
`; } function updateHeader(count) { const el = document.getElementById('pv-subtitle'); if (!el) return; const total = count !== undefined ? count : filterModels().length; el.textContent = total + ' modèle' + (total > 1 ? 's' : ''); } // ─── Detail modal ───────────────────────────────────────────────────────────── function openDetail(id) { const m = allModels.find(x => x.id === id); if (!m) return; const ext = m.file_type.toLowerCase(); const thumbSrc = m.thumb_filename ? `${PV_BASE}/thumbs/${m.thumb_filename}` : null; const typeIcon = { stl: '🟣', '3mf': '🔵', gcode: '🟢', g: '🟢', gco: '🟢' }[ext] || '📄'; const isGcode = ['gcode','gco','g'].includes(ext); document.getElementById('pv-detail-body').innerHTML = `
${thumbSrc ? `` : typeIcon}
${escHtml(m.name)}
${m.description ? `
${escHtml(m.description)}
` : ''} ${m.tags ? `
${m.tags.split(',').filter(Boolean).map(t => `${escHtml(t.trim())}`).join('')}
` : ''}
Type${escHtml(m.file_type.toUpperCase())}
Catégorie${escHtml(m.category)}
Taille${escHtml(fmtSize(m.file_size))}
Ajouté le${escHtml(fmtDate(m.created_at))}
${m.dim_x > 0 ? `
Dimensions${Math.round(m.dim_x)}×${Math.round(m.dim_y)}×${Math.round(m.dim_z)} mm
` : ''} ${m.volume > 0 ? `
Volume${m.volume.toFixed(2)} cm³
` : ''} ${isGcode && m.gcode_print_time ? `
Temps impression${escHtml(m.gcode_print_time)}
` : ''} ${isGcode && m.gcode_filament ? `
Filament${escHtml(m.gcode_filament)}
` : ''} ${isGcode && m.gcode_nozzle_temp ? `
Buse${escHtml(m.gcode_nozzle_temp)}
` : ''} ${isGcode && m.gcode_bed_temp ? `
Plateau${escHtml(m.gcode_bed_temp)}
` : ''}
`; document.getElementById('pv-detail-3d-btn').onclick = () => window.open(`${PV_BASE}/model/${m.id}`, '_blank'); document.getElementById('pv-detail-dl-btn').href = `${API}?action=file&id=${m.id}`; document.getElementById('pv-detail-del-btn').onclick = () => deleteModel(m.id, m.name); document.getElementById('pv-detail-modal').classList.add('show'); } async function deleteModel(id, name) { if (!confirm(`Supprimer "${name}" ?`)) return; try { await api('models', 'DELETE', null, '&id=' + id); document.getElementById('pv-detail-modal').classList.remove('show'); toast('Modèle supprimé'); allModels = allModels.filter(m => m.id !== id); renderSidebar(); renderModels(); } catch (e) { toast(e.message, 'error'); } } // ─── Upload modal ───────────────────────────────────────────────────────────── let uploadFile = null; function openUpload() { uploadFile = null; document.getElementById('pv-file-input').value = ''; document.getElementById('pv-upload-filename').textContent = ''; document.getElementById('pv-upload-name').value = ''; document.getElementById('pv-upload-desc').value = ''; document.getElementById('pv-upload-tags').value = ''; const catSel = document.getElementById('pv-upload-cat'); catSel.innerHTML = categories.map(c => ``).join(''); document.getElementById('pv-upload-progress').style.display = 'none'; document.getElementById('pv-upload-modal').classList.add('show'); } function setUploadFile(f) { const allowed = ['stl', '3mf', 'gcode', 'gco', 'g']; const ext = f.name.split('.').pop().toLowerCase(); if (!allowed.includes(ext)) { toast('Format non supporté (STL, 3MF, GCode)', 'error'); return; } uploadFile = f; document.getElementById('pv-upload-filename').textContent = f.name; if (!document.getElementById('pv-upload-name').value) { document.getElementById('pv-upload-name').value = f.name.replace(/\.[^.]+$/, '').replace(/[_-]/g, ' '); } } async function saveUpload() { if (!uploadFile) { toast('Choisissez un fichier', 'error'); return; } const name = document.getElementById('pv-upload-name').value.trim(); if (!name) { toast('Nom requis', 'error'); return; } const fd = new FormData(); fd.append('file', uploadFile); fd.append('name', name); fd.append('description', document.getElementById('pv-upload-desc').value); fd.append('category', document.getElementById('pv-upload-cat').value); fd.append('tags', document.getElementById('pv-upload-tags').value); const prog = document.getElementById('pv-upload-progress'); const bar = document.getElementById('pv-upload-bar'); prog.style.display = ''; bar.style.width = '30%'; try { await api('models', 'POST', fd); bar.style.width = '100%'; setTimeout(() => { document.getElementById('pv-upload-modal').classList.remove('show'); toast('Modèle ajouté ✓'); loadModels(); }, 300); } catch (e) { prog.style.display = 'none'; toast(e.message, 'error'); } }