From 7272772c9fada42e35270ee238385256853f58b4 Mon Sep 17 00:00:00 2001 From: perco Date: Fri, 15 May 2026 10:14:03 +0200 Subject: [PATCH] Add PrintVault module: 3D print file manager integrated in HouseHub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - printvault.php: SPA with model grid, sidebar (categories + type filters), search - modules/printvault/api.php: PHP proxy to PrintVault API (auth + CORS) - modules/printvault/assets/printvault.js: model grid, upload modal, detail modal - modules/printvault/assets/printvault.css: full light/dark theme - Upload: drag&drop + form (name, category, tags) - Detail modal: metadata, dimensions, GCode info, download, delete, "Voir en 3D" โ†’ PrintVault - Integrated in header nav, home page, settings; translations fr/en/ca Co-Authored-By: Claude Sonnet 4.6 --- header.php | 2 + includes/lang/ca.php | 4 + includes/lang/en.php | 4 + includes/lang/fr.php | 4 + index.php | 9 + modules/printvault/api.php | 111 +++++++++ modules/printvault/assets/printvault.css | 190 +++++++++++++++ modules/printvault/assets/printvault.js | 292 +++++++++++++++++++++++ printvault.php | 125 ++++++++++ settings.php | 3 +- 10 files changed, 743 insertions(+), 1 deletion(-) create mode 100644 modules/printvault/api.php create mode 100644 modules/printvault/assets/printvault.css create mode 100644 modules/printvault/assets/printvault.js create mode 100644 printvault.php 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'; + @@ -106,6 +107,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr'; โœ… ๐Ÿ›’ ๐Ÿ“ฑ + ๐Ÿ–จ๏ธ โš™๏ธ 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): ?> + + +
๐Ÿ–จ๏ธ
+

+
+ +
+ + 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 @@ + + + +
+ +
+ + + + + +
+
+
+

๐Ÿ–จ๏ธ PrintVault

+
+
+
+ Ouvrir PrintVault โ†— + +
+
+ +
+
+
๐Ÿ–จ๏ธ
+

Chargementโ€ฆ

+
+
+
+
+ + +
+
+
+

Dรฉtail du modรจle

+ +
+
+ +
+
+ + +
+
+
+

Uploader un modรจle

+ +
+
+ +
+
๐Ÿ“
+
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')], ]; ?>