diff --git a/header.php b/header.php
index b6a150d..10ef055 100644
--- a/header.php
+++ b/header.php
@@ -57,6 +57,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
= tr('menu_todo') ?>
= tr('menu_groceries') ?>
= tr('menu_calendar_ios') ?>
+ = tr('menu_printvault') ?>
@@ -106,6 +107,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
โ
= tr('menu_todo') ?>
๐ = tr('menu_groceries') ?>
๐ฑ = tr('menu_calendar_ios') ?>
+ ๐จ๏ธ = tr('menu_printvault') ?>
โ๏ธ Paramรจtres
๐ก๏ธ Admin
diff --git a/includes/lang/ca.php b/includes/lang/ca.php
index b64db3b..8eb1107 100644
--- a/includes/lang/ca.php
+++ b/includes/lang/ca.php
@@ -148,6 +148,10 @@ return [
'cta_check' => 'Marcar',
'cta_cart' => 'Anar a comprar',
'cta_sync' => 'Sincronitzar',
+ 'mod_printvault_name' => 'PrintVault',
+ 'mod_printvault_desc' => 'Gestioneu els vostres fitxers d\'impressiรณ 3D โ STL, 3MF, GCode.',
+ 'menu_printvault' => 'PrintVault',
+ 'cta_print' => 'Imprimir',
// ==========================================
// FAMILY CALENDAR
diff --git a/includes/lang/en.php b/includes/lang/en.php
index 05ae25b..e72de43 100644
--- a/includes/lang/en.php
+++ b/includes/lang/en.php
@@ -149,6 +149,10 @@ return [
'cta_check' => 'Check off',
'cta_cart' => 'Go shopping',
'cta_sync' => 'Sync',
+ 'mod_printvault_name' => 'PrintVault',
+ 'mod_printvault_desc' => 'Manage your 3D print files โ STL, 3MF, GCode with interactive viewer.',
+ 'menu_printvault' => 'PrintVault',
+ 'cta_print' => 'Print',
// ==========================================
// FAMILY CALENDAR
diff --git a/includes/lang/fr.php b/includes/lang/fr.php
index 532e1b1..f228a3e 100644
--- a/includes/lang/fr.php
+++ b/includes/lang/fr.php
@@ -151,6 +151,10 @@ return [
'cta_check' => 'Cocher',
'cta_cart' => 'Faire les courses',
'cta_sync' => 'Synchroniser',
+ 'mod_printvault_name' => 'PrintVault',
+ 'mod_printvault_desc' => 'Gรฉrez vos fichiers d\'impression 3D โ STL, 3MF, GCode avec viewer interactif.',
+ 'menu_printvault' => 'PrintVault',
+ 'cta_print' => 'Imprimer',
// ==========================================
// FAMILY CALENDAR
diff --git a/index.php b/index.php
index 6f0977c..84b8317 100644
--- a/index.php
+++ b/index.php
@@ -127,6 +127,15 @@ if ($_has_custom_bg): ?>
+
+
+ ๐จ๏ธ
+ = tr('mod_printvault_name') ?>
+ = tr('mod_printvault_desc') ?>
+ = tr('cta_print') ?>
+
+
+
diff --git a/modules/printvault/api.php b/modules/printvault/api.php
new file mode 100644
index 0000000..ce97211
--- /dev/null
+++ b/modules/printvault/api.php
@@ -0,0 +1,111 @@
+ false, 'error' => $e->getMessage()]); exit;
+});
+
+define('PV_BASE', 'http://192.168.1.29:9500');
+
+function pvOk($d) { echo json_encode(['ok' => true, 'data' => $d], JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE); exit; }
+function pvErr($m, $c = 400) { http_response_code($c); echo json_encode(['ok' => false, 'error' => $m]); exit; }
+
+function pvProxy(string $url, string $method = 'GET', ?string $body = null, string $contentType = 'application/json'): array {
+ $ch = curl_init($url);
+ curl_setopt_array($ch, [
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => 15,
+ CURLOPT_CUSTOMREQUEST => $method,
+ ]);
+ if ($body !== null) {
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: ' . $contentType]);
+ }
+ $resp = curl_exec($ch);
+ $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+ return ['body' => $resp, 'code' => $code];
+}
+
+$method = $_SERVER['REQUEST_METHOD'];
+$action = $_GET['action'] ?? '';
+
+// โโ MODELS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+if ($action === 'models') {
+ if ($method === 'GET') {
+ $qs = http_build_query(array_intersect_key($_GET, array_flip(['category','file_type','search','limit','offset'])));
+ $r = pvProxy(PV_BASE . '/api/models?' . $qs);
+ if ($r['code'] !== 200) pvErr('PrintVault inaccessible', 502);
+ pvOk(json_decode($r['body'], true));
+ }
+ if ($method === 'DELETE') {
+ $id = $_GET['id'] ?? null; if (!$id) pvErr('ID manquant');
+ $r = pvProxy(PV_BASE . '/api/models/' . intval($id), 'DELETE');
+ if ($r['code'] === 200) pvOk(['deleted' => true]);
+ pvErr('Erreur suppression', 500);
+ }
+ if ($method === 'POST') {
+ // Upload: forward multipart to PrintVault
+ if (empty($_FILES['file'])) pvErr('Fichier manquant');
+ $f = $_FILES['file'];
+ if ($f['error'] !== UPLOAD_ERR_OK) pvErr('Erreur upload: ' . $f['error']);
+
+ $postFields = [
+ 'file' => new CURLFile($f['tmp_name'], $f['type'] ?: 'application/octet-stream', $f['name']),
+ 'name' => $_POST['name'] ?? pathinfo($f['name'], PATHINFO_FILENAME),
+ 'description' => $_POST['description'] ?? '',
+ 'category' => $_POST['category'] ?? 'Non classรฉ',
+ 'tags' => $_POST['tags'] ?? '',
+ ];
+
+ $ch = curl_init(PV_BASE . '/api/models');
+ curl_setopt_array($ch, [
+ CURLOPT_POST => true,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => 60,
+ CURLOPT_POSTFIELDS => $postFields,
+ ]);
+ $resp = curl_exec($ch);
+ $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($code === 200) pvOk(json_decode($resp, true));
+ pvErr('Upload รฉchouรฉ (' . $code . '): ' . substr($resp, 0, 200), 500);
+ }
+}
+
+// โโ CATEGORIES โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+if ($action === 'categories') {
+ $r = pvProxy(PV_BASE . '/api/categories');
+ if ($r['code'] !== 200) pvErr('PrintVault inaccessible', 502);
+ pvOk(json_decode($r['body'], true));
+}
+
+// โโ FILE DOWNLOAD (proxy) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+if ($action === 'file') {
+ $id = intval($_GET['id'] ?? 0); if (!$id) pvErr('ID manquant');
+ $info = pvProxy(PV_BASE . '/api/models/' . $id);
+ if ($info['code'] !== 200) pvErr('Modรจle introuvable', 404);
+ $m = json_decode($info['body'], true);
+
+ $ch = curl_init(PV_BASE . '/api/models/' . $id . '/file');
+ curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30]);
+ $data = curl_exec($ch);
+ $mime = curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?: 'application/octet-stream';
+ curl_close($ch);
+
+ ob_end_clean();
+ header('Content-Type: ' . $mime);
+ header('Content-Disposition: attachment; filename="' . rawurlencode($m['original_name'] ?? 'model') . '"');
+ header('Cache-Control: no-cache');
+ echo $data;
+ exit;
+}
+
+pvErr('Action inconnue', 404);
diff --git a/modules/printvault/assets/printvault.css b/modules/printvault/assets/printvault.css
new file mode 100644
index 0000000..a0a3232
--- /dev/null
+++ b/modules/printvault/assets/printvault.css
@@ -0,0 +1,190 @@
+/* PrintVault module */
+
+.pv-layout {
+ display: grid;
+ grid-template-columns: 220px 1fr;
+ height: calc(100vh - 64px);
+ overflow: hidden;
+}
+.pv-sidebar {
+ border-right: 1px solid var(--border-light);
+ background: var(--bg-panel);
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+}
+.pv-sidebar-top {
+ padding: .75rem;
+ border-bottom: 1px solid var(--border-light);
+ position: sticky; top: 0; background: var(--bg-panel); z-index: 1;
+ display: flex; flex-direction: column; gap: .5rem;
+}
+.pv-search {
+ display: flex; align-items: center; gap: .4rem;
+ background: var(--bg-page); border: 1px solid var(--border-light);
+ border-radius: 8px; padding: .35rem .6rem;
+}
+.pv-search input { border: none; outline: none; background: transparent; font-size: .85rem; color: var(--text-main); flex: 1; }
+.pv-search input::placeholder { color: var(--text-muted); }
+.pv-main { overflow-y: auto; background: var(--bg-page); }
+
+/* Sidebar nav */
+.pv-nav-section { font-size: .7rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); padding: .75rem .75rem .25rem; }
+.pv-nav-item {
+ display: flex; align-items: center; gap: .5rem;
+ padding: .45rem .75rem; cursor: pointer;
+ font-size: .85rem; color: var(--text-muted);
+ border-radius: 8px; margin: 1px .5rem;
+ transition: background .12s, color .12s;
+}
+.pv-nav-item:hover { background: var(--bg-page); color: var(--text-main); }
+.pv-nav-item.active { background: #eff6ff; color: var(--primary); font-weight: 600; }
+.pv-nav-badge { margin-left: auto; font-size: .7rem; font-weight: 700; background: var(--bg-page); color: var(--text-muted); padding: 1px 6px; border-radius: 999px; }
+.pv-nav-item.active .pv-nav-badge { background: #dbeafe; color: var(--primary); }
+.pv-cat-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
+
+/* File type filter chips */
+.pv-type-chips { display: flex; gap: .3rem; flex-wrap: wrap; padding: .25rem .75rem .5rem; }
+.pv-chip {
+ font-size: .72rem; font-weight: 600; padding: .2rem .5rem;
+ border-radius: 999px; cursor: pointer; border: 1.5px solid var(--border-light);
+ color: var(--text-muted); transition: all .12s;
+}
+.pv-chip:hover { border-color: var(--primary); color: var(--primary); }
+.pv-chip.active { background: var(--primary); border-color: var(--primary); color: #fff; }
+.pv-chip.stl.active { background: #8b5cf6; border-color: #8b5cf6; }
+.pv-chip.3mf.active { background: #3b82f6; border-color: #3b82f6; }
+.pv-chip.gcode.active { background: #10b981; border-color: #10b981; }
+
+/* Main header */
+.pv-main-header {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 1rem 1.5rem; background: var(--bg-panel);
+ border-bottom: 1px solid var(--border-light); gap: .75rem; flex-wrap: wrap;
+}
+.pv-main-header h2 { font-size: 1rem; font-weight: 700; margin: 0; }
+.pv-subtitle { font-size: .8rem; color: var(--text-muted); }
+
+/* Models grid */
+.pv-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 1rem;
+ padding: 1.25rem 1.5rem 2rem;
+}
+.pv-card {
+ background: var(--bg-panel); border: 1px solid var(--border-light);
+ border-radius: 12px; overflow: hidden; cursor: pointer;
+ transition: border-color .15s, box-shadow .12s;
+ display: flex; flex-direction: column;
+}
+.pv-card:hover { border-color: #cbd5e1; box-shadow: 0 4px 16px rgba(0,0,0,.08); }
+.pv-card-thumb {
+ aspect-ratio: 4/3; background: #f1f5f9;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 3rem; overflow: hidden;
+}
+.pv-card-thumb img { width: 100%; height: 100%; object-fit: cover; }
+.pv-card-body { padding: .75rem; flex: 1; display: flex; flex-direction: column; gap: .25rem; }
+.pv-card-name { font-size: .875rem; font-weight: 600; color: var(--text-main); line-height: 1.3; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
+.pv-card-meta { font-size: .72rem; color: var(--text-muted); display: flex; gap: .4rem; flex-wrap: wrap; margin-top: auto; padding-top: .25rem; }
+.pv-type-badge { font-size: .68rem; font-weight: 700; padding: .1rem .4rem; border-radius: 4px; text-transform: uppercase; }
+.pv-type-stl { background: #ede9fe; color: #7c3aed; }
+.pv-type-3mf { background: #dbeafe; color: #1d4ed8; }
+.pv-type-gcode { background: #dcfce7; color: #15803d; }
+.pv-type-g { background: #dcfce7; color: #15803d; }
+.pv-type-gco { background: #dcfce7; color: #15803d; }
+
+/* Empty state */
+.pv-empty { text-align: center; padding: 4rem 2rem; color: var(--text-muted); }
+.pv-empty .icon { font-size: 3rem; opacity: .3; margin-bottom: 1rem; }
+
+/* Modal */
+.pv-modal-backdrop {
+ display: none; position: fixed; inset: 0; z-index: 200;
+ background: rgba(15,23,42,.5); backdrop-filter: blur(4px);
+ align-items: center; justify-content: center; padding: 1rem;
+}
+.pv-modal-backdrop.show { display: flex; }
+.pv-modal {
+ background: var(--bg-panel); border: 1px solid var(--border-light);
+ border-radius: 14px; width: 100%; max-width: 520px;
+ max-height: 92vh; display: flex; flex-direction: column;
+ box-shadow: 0 20px 40px rgba(0,0,0,.15);
+ animation: pvModalIn .18s ease;
+}
+@keyframes pvModalIn { from{opacity:0;transform:scale(.96) translateY(-8px)} to{opacity:1;transform:none} }
+.pv-modal-header { display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.25rem; border-bottom: 1px solid var(--border-light); }
+.pv-modal-header h3 { font-size: 1rem; font-weight: 700; margin: 0; }
+.pv-modal-close { background: none; border: none; cursor: pointer; color: var(--text-muted); font-size: 1.2rem; border-radius: 6px; padding: .2rem .4rem; }
+.pv-modal-close:hover { background: var(--bg-page); }
+.pv-modal-body { padding: 1.25rem; overflow-y: auto; display: flex; flex-direction: column; gap: .9rem; }
+.pv-modal-footer { padding: 1rem 1.25rem; border-top: 1px solid var(--border-light); display: flex; justify-content: flex-end; gap: .5rem; }
+
+/* Upload zone */
+.pv-drop-zone {
+ border: 2px dashed var(--border-light); border-radius: 10px;
+ padding: 2rem; text-align: center; cursor: pointer;
+ transition: border-color .15s, background .15s; color: var(--text-muted);
+}
+.pv-drop-zone:hover, .pv-drop-zone.over { border-color: var(--primary); background: #eff6ff; color: var(--primary); }
+.pv-drop-zone .icon { font-size: 2rem; margin-bottom: .5rem; }
+.pv-file-chosen { font-size: .82rem; color: var(--primary); font-weight: 600; margin-top: .4rem; }
+
+/* Form */
+.form-group { display: flex; flex-direction: column; gap: .35rem; }
+.form-label { font-size: .8rem; font-weight: 600; color: var(--text-muted); }
+.form-control { background: #fff; border: 1px solid var(--border-light); color: var(--text-main); border-radius: 8px; padding: .5rem .75rem; font-size: .875rem; width: 100%; outline: none; transition: border-color .15s; font-family: inherit; }
+.form-control:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(59,130,246,.1); }
+
+/* Buttons */
+.btn { display: inline-flex; align-items: center; gap: .4rem; padding: .4rem .85rem; border-radius: 8px; font-size: .875rem; font-weight: 500; cursor: pointer; border: 1px solid transparent; transition: all .15s; white-space: nowrap; }
+.btn-primary { background: var(--primary); color: #fff; border-color: var(--primary); }
+.btn-primary:hover { background: var(--primary-dark); }
+.btn-secondary { background: var(--bg-panel); color: var(--text-main); border-color: var(--border-light); }
+.btn-secondary:hover { background: var(--bg-page); }
+.btn-danger { background: #fff; color: var(--danger); border-color: #fecaca; }
+.btn-danger:hover { background: #fef2f2; }
+.btn-sm { padding: .25rem .55rem; font-size: .78rem; }
+.btn-ghost { background: transparent; border: none; color: var(--text-muted); padding: .2rem .4rem; }
+.btn-ghost:hover { color: var(--text-main); background: var(--bg-page); border-radius: 6px; }
+.btn-icon { padding: .3rem; min-width: 30px; justify-content: center; }
+
+/* Detail panel */
+.pv-detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem .75rem; }
+.pv-detail-item { display: flex; flex-direction: column; gap: .1rem; }
+.pv-detail-label { font-size: .72rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; }
+.pv-detail-value { font-size: .875rem; color: var(--text-main); }
+.pv-detail-thumb { width: 100%; aspect-ratio: 4/3; background: #f1f5f9; border-radius: 10px; overflow: hidden; display: flex; align-items: center; justify-content: center; font-size: 4rem; margin-bottom: .75rem; }
+.pv-detail-thumb img { width: 100%; height: 100%; object-fit: cover; }
+
+/* Toasts */
+.pv-toast-container { position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 9999; display: flex; flex-direction: column; gap: .5rem; }
+.pv-toast { background: var(--bg-panel); border: 1px solid var(--border-light); border-radius: 10px; padding: .7rem 1rem; font-size: .875rem; min-width: 200px; color: var(--text-main); box-shadow: 0 4px 16px rgba(0,0,0,.12); border-left: 4px solid var(--primary); animation: pvToastIn .2s ease; }
+.pv-toast.error { border-left-color: var(--danger); }
+@keyframes pvToastIn { from{opacity:0;transform:translateX(16px)} to{opacity:1;transform:none} }
+
+/* Upload progress */
+.pv-progress { height: 4px; background: var(--border-light); border-radius: 999px; overflow: hidden; margin-top: .4rem; }
+.pv-progress-bar { height: 100%; background: var(--primary); border-radius: 999px; transition: width .3s; }
+
+/* Responsive */
+@media (max-width: 768px) {
+ .pv-layout { grid-template-columns: 1fr; height: auto; }
+ .pv-sidebar { display: none; }
+ .pv-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); padding: .75rem; }
+ .pv-main-header { padding: .75rem 1rem; }
+}
+
+/* Dark mode */
+[data-theme="dark"] .pv-sidebar { background: var(--bg-panel); }
+[data-theme="dark"] .pv-card { background: var(--bg-panel); border-color: var(--border-light); }
+[data-theme="dark"] .pv-card-thumb { background: #1c2128; }
+[data-theme="dark"] .pv-modal { background: var(--bg-panel); }
+[data-theme="dark"] .form-control { background: #1c2128; border-color: var(--border-light); color: var(--text-main); }
+[data-theme="dark"] .pv-drop-zone { border-color: var(--border-light); }
+[data-theme="dark"] .pv-drop-zone:hover { background: rgba(59,130,246,.1); }
+[data-theme="dark"] .pv-main-header { background: var(--bg-panel); }
+[data-theme="dark"] .pv-type-stl { background: rgba(139,92,246,.2); }
+[data-theme="dark"] .pv-type-3mf { background: rgba(59,130,246,.2); }
+[data-theme="dark"] .pv-type-gcode, [data-theme="dark"] .pv-type-g, [data-theme="dark"] .pv-type-gco { background: rgba(16,185,129,.2); }
diff --git a/modules/printvault/assets/printvault.js b/modules/printvault/assets/printvault.js
new file mode 100644
index 0000000..9235c00
--- /dev/null
+++ b/modules/printvault/assets/printvault.js
@@ -0,0 +1,292 @@
+// 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');
+ }
+}
diff --git a/printvault.php b/printvault.php
new file mode 100644
index 0000000..ef8338c
--- /dev/null
+++ b/printvault.php
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
๐จ๏ธ
+
Chargementโฆ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
๐
+
Glissez un fichier ici ou cliquez pour choisir
+
STL ยท 3MF ยท GCode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/settings.php b/settings.php
index 2af2b61..53225c0 100644
--- a/settings.php
+++ b/settings.php
@@ -36,7 +36,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'set_modules' && $family_id) {
- $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'groceries', 'calendar_ios'];
+ $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'groceries', 'calendar_ios', 'printvault'];
$enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m])));
if (empty($enabled)) {
$error = "Vous devez garder au moins un module actif.";
@@ -309,6 +309,7 @@ require __DIR__ . '/header.php';
'todo' => ['icon' => 'โ
', 'label' => tr('menu_todo')],
'groceries' => ['icon' => '๐', 'label' => tr('menu_groceries')],
'calendar_ios' => ['icon' => '๐ฑ', 'label' => tr('menu_calendar_ios')],
+ 'printvault' => ['icon' => '๐จ๏ธ', 'label' => tr('menu_printvault')],
];
?>