// 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 = ``;
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');
}
}