diff --git a/.gitignore b/.gitignore index c0bb667..a52b037 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ vite.config.ts.timestamp-* .vscode/ +static/uploads/ diff --git a/src/lib/components/Navbar.svelte b/src/lib/components/Navbar.svelte index 807b7fb..64d1ed4 100644 --- a/src/lib/components/Navbar.svelte +++ b/src/lib/components/Navbar.svelte @@ -10,6 +10,7 @@ { name: "Spools", href: "/spools", icon: "mdi:cylinder" }, { name: "Printers", href: "/printers", icon: "mdi:printer-3d" }, { name: "Prints", href: "/prints", icon: "mdi:cube-outline" }, + { name: "Library", href: "/library", icon: "mdi:cube-scan" }, { name: "Analytics", href: "/analytics", icon: "mdi:chart-line" }, ...($page.data.user?.role === "Admin" ? [ diff --git a/src/lib/components/STLViewer.svelte b/src/lib/components/STLViewer.svelte new file mode 100644 index 0000000..b1b1bad --- /dev/null +++ b/src/lib/components/STLViewer.svelte @@ -0,0 +1,216 @@ + + +
+ + diff --git a/src/lib/components/prints/EditPrintModal.svelte b/src/lib/components/prints/EditPrintModal.svelte index 7bd4cf2..060403f 100644 --- a/src/lib/components/prints/EditPrintModal.svelte +++ b/src/lib/components/prints/EditPrintModal.svelte @@ -1,5 +1,6 @@ @@ -52,7 +110,17 @@
{ + use:enhance={async ({ formData }) => { + isSubmitting = true; + + // Upload STL file first if selected + if (stlFile) { + const uploadedPath = await uploadSTL(); + if (uploadedPath) { + formData.set("stl_file", uploadedPath); + } + } + // Convert hours + minutes to total minutes const hours = Number(formData.get("duration_hours") || 0); const mins = Number(formData.get("duration_mins") || 0); @@ -66,7 +134,6 @@ String(elapsedHours * 60 + elapsedMins), ); - isSubmitting = true; return async ({ update }) => { await update(); isSubmitting = false; @@ -146,6 +213,95 @@ + +
+ + + + {#if print.stl_file && browser && !stlFile} + +
+ {#await import("$lib/components/STLViewer.svelte") then { default: STLViewer }} + + {/await} +
+

+ Click below to replace with a new model +

+ {/if} + + + {#if uploadStatus === "Uploading..." && uploadProgress > 0} + +
+
+ {stlFile?.name} + {uploadProgress}% +
+
+
+
+
+ {:else} +
+ + {#if stlFile} + + {/if} +
+ {/if} +
+ {#if editStatus === "In Progress"}
diff --git a/src/lib/components/prints/LogPrintModal.svelte b/src/lib/components/prints/LogPrintModal.svelte index 8b88bb3..9f71b92 100644 --- a/src/lib/components/prints/LogPrintModal.svelte +++ b/src/lib/components/prints/LogPrintModal.svelte @@ -1,260 +1,473 @@ - { - // Convert hours + minutes to total minutes - const hours = Number(formData.get('duration_hours') || 0); - const mins = Number(formData.get('duration_mins') || 0); - formData.set('duration_minutes', String(hours * 60 + mins)); + { + isSubmitting = true; - // Convert elapsed hours + minutes to total minutes - const elapsedHours = Number(formData.get('elapsed_hours') || 0); - const elapsedMins = Number(formData.get('elapsed_mins') || 0); - formData.set('elapsed_minutes', String(elapsedHours * 60 + elapsedMins)); + // Upload STL file first if selected + if (stlFile) { + const uploadedPath = await uploadSTL(); + if (uploadedPath) { + formData.set("stl_file", uploadedPath); + } + } - isSubmitting = true; - return async ({ update }) => { - await update(); - isSubmitting = false; - handleClose(); - }; - }} - class="space-y-4" - > - -
- - -
- - - - -
-
+ // Convert hours + minutes to total minutes + const hours = Number(formData.get("duration_hours") || 0); + const mins = Number(formData.get("duration_mins") || 0); + formData.set("duration_minutes", String(hours * 60 + mins)); - + // Convert elapsed hours + minutes to total minutes + const elapsedHours = Number(formData.get("elapsed_hours") || 0); + const elapsedMins = Number(formData.get("elapsed_mins") || 0); + formData.set( + "elapsed_minutes", + String(elapsedHours * 60 + elapsedMins), + ); -
-
- - - -
-
- - - -
-
+ return async ({ update }) => { + await update(); + isSubmitting = false; + handleClose(); + }; + }} + class="space-y-4" + > + +
+ + +
+ + + + +
+
- {#if selectedStatus === "In Progress"} - -
-

- - Enter the expected total print time and how long it's been running. -

-
-
- - -
-
- - hr -
-
- - min -
-
-
-
- - -
-
- - hr -
-
- - min -
-
-
-
-
- - -
-
- {:else} - -
-
- - -
-
- - hr -
-
- - min -
-
-
-
- - -
-
- {/if} + -
- - -
- + +
+ + + + {#if uploadStatus === "Uploading..." && uploadProgress > 0} + +
+
+ {stlFile?.name} + {uploadProgress}% +
+
+
+
+
+ {:else} + +
+ + {#if stlFile} + + {/if} +
+ {/if} +
+ +
+
+ + + +
+
+ + + +
+
+ + {#if selectedStatus === "In Progress"} + +
+

+ + Enter the expected total print time and how long it's been running. +

+
+
+ + +
+
+ + hr +
+
+ + min +
+
+
+
+ + +
+
+ + hr +
+
+ + min +
+
+
+
+
+ + +
+
+ {:else} + +
+
+ + +
+
+ + hr +
+
+ + min +
+
+
+
+ + +
+
+ {/if} + +
+ + +
+
diff --git a/src/lib/models/PrintJob.ts b/src/lib/models/PrintJob.ts index a25f68e..66566aa 100644 --- a/src/lib/models/PrintJob.ts +++ b/src/lib/models/PrintJob.ts @@ -47,6 +47,9 @@ const printJobSchema = new mongoose.Schema({ type: Date }, notes: String, + stl_file: { + type: String // Path to uploaded STL file + }, date: { type: Date, default: Date.now diff --git a/src/routes/api/upload-stl/+server.ts b/src/routes/api/upload-stl/+server.ts new file mode 100644 index 0000000..7047419 --- /dev/null +++ b/src/routes/api/upload-stl/+server.ts @@ -0,0 +1,54 @@ +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { writeFile, mkdir } from 'fs/promises'; +import { existsSync } from 'fs'; +import path from 'path'; + +const UPLOAD_DIR = 'static/uploads/models'; +const ALLOWED_EXTENSIONS = ['.stl', '.obj']; + +export const POST: RequestHandler = async ({ request, locals }) => { + if (!locals.user) { + throw error(401, 'Unauthorized'); + } + + const formData = await request.formData(); + const file = formData.get('stl') as File; // Keep 'stl' for backward compatibility + + if (!file || file.size === 0) { + throw error(400, 'No file provided'); + } + + // Validate file type + const fileName = file.name.toLowerCase(); + const extension = '.' + fileName.split('.').pop(); + + if (!ALLOWED_EXTENSIONS.includes(extension)) { + throw error(400, 'Only STL and OBJ files are allowed'); + } + + // Create upload directory if it doesn't exist + if (!existsSync(UPLOAD_DIR)) { + await mkdir(UPLOAD_DIR, { recursive: true }); + } + + // Generate unique filename + const timestamp = Date.now(); + const sanitizedName = file.name.replace(/[^a-zA-Z0-9.-]/g, '_'); + const uniqueFileName = `${locals.user.id}_${timestamp}_${sanitizedName}`; + const filePath = path.join(UPLOAD_DIR, uniqueFileName); + + // Write file to disk + const buffer = Buffer.from(await file.arrayBuffer()); + await writeFile(filePath, buffer); + + // Return the public path + const publicPath = `/uploads/models/${uniqueFileName}`; + + return json({ + success: true, + path: publicPath, + fileName: file.name, + fileType: extension.slice(1).toUpperCase() + }); +}; diff --git a/src/routes/library/+page.server.ts b/src/routes/library/+page.server.ts new file mode 100644 index 0000000..c0f1285 --- /dev/null +++ b/src/routes/library/+page.server.ts @@ -0,0 +1,22 @@ +import { PrintJob } from '$lib/models/PrintJob'; +import { connectDB } from '$lib/server/db'; +import type { PageServerLoad } from './$types'; +import { redirect } from '@sveltejs/kit'; + +export const load: PageServerLoad = async ({ locals }) => { + if (!locals.user) throw redirect(303, '/login'); + + await connectDB(); + + // Get all prints with STL files + const printsWithSTL = await PrintJob.find({ + user_id: locals.user.id, + stl_file: { $exists: true, $nin: [null, ''] } + }) + .sort({ date: -1 }) + .lean(); + + return { + models: JSON.parse(JSON.stringify(printsWithSTL)) + }; +}; diff --git a/src/routes/library/+page.svelte b/src/routes/library/+page.svelte new file mode 100644 index 0000000..0402fcb --- /dev/null +++ b/src/routes/library/+page.svelte @@ -0,0 +1,227 @@ + + +
+
+

Model Library

+

+ Browse your 3D model collection ({models.length} models) +

+
+ + {#if models.length === 0} + + +

+ No Models Yet +

+

+ Upload STL files when logging prints to build your model + library. Go to Prints to add your first model. +

+
+ {:else} +
+ {#each models as model} + + +
openViewer(model)} + > + + +
+ +
+ + + View Model + +
+
+ + +
+

+ {model.name} +

+

+ {formatDate(model.date)} +

+
+ {#if model.status === "Success"} + + + Printed + + {:else if model.status === "In Progress"} + + + Printing + + {:else if model.status === "Fail"} + + + Failed + + {/if} +
+
+
+
+ {/each} +
+ {/if} +
+ + +{#if showViewer && selectedModel && browser} + + +
+ +
e.stopPropagation()} + > +
+

+ {selectedModel.name} +

+

+ {formatDate(selectedModel.date)} +

+
+ +
+ + +
e.stopPropagation()} + > + {#await import("$lib/components/STLViewer.svelte") then { default: STLViewer }} + + {/await} +
+ + +
e.stopPropagation()} + > + {#if selectedModel.filament_used_g} +
+ Filament: + {selectedModel.filament_used_g}g +
+ {/if} + {#if selectedModel.duration_minutes} +
+ Duration: + {Math.floor(selectedModel.duration_minutes / 60)}h {selectedModel.duration_minutes % + 60}m +
+ {/if} + {#if selectedModel.calculated_cost_filament} +
+ Cost: + ${selectedModel.calculated_cost_filament.toFixed(2)} +
+ {/if} +
+
+{/if} + + diff --git a/src/routes/prints/+page.server.ts b/src/routes/prints/+page.server.ts index 51f2153..9e7504d 100644 --- a/src/routes/prints/+page.server.ts +++ b/src/routes/prints/+page.server.ts @@ -44,6 +44,7 @@ export const actions: Actions = { const status = formData.get('status'); const manual_cost = formData.get('manual_cost'); const elapsed_minutes = formData.get('elapsed_minutes'); + const stl_file = formData.get('stl_file'); if (!spool_id || !printer_id || !filament_used_g) { return fail(400, { missing: true }); @@ -110,6 +111,7 @@ export const actions: Actions = { calculated_cost_energy: Number(costEnergy.toFixed(2)), status, started_at: startedAt, + stl_file: stl_file || null, date: new Date() }); @@ -139,6 +141,7 @@ export const actions: Actions = { const elapsed_minutes = formData.get('elapsed_minutes'); const printer_id = formData.get('printer_id'); const spool_id = formData.get('spool_id'); + const stl_file = formData.get('stl_file'); if (!id || !name) { return fail(400, { missing: true }); @@ -215,6 +218,10 @@ export const actions: Actions = { if (spool_id) { updateData.spool_id = spool_id; } + // Update STL file if provided + if (stl_file) { + updateData.stl_file = stl_file; + } await PrintJob.findOneAndUpdate( { _id: id, user_id: locals.user.id }, diff --git a/src/routes/prints/+page.svelte b/src/routes/prints/+page.svelte index 4d7b6b9..4562a3e 100644 --- a/src/routes/prints/+page.svelte +++ b/src/routes/prints/+page.svelte @@ -75,9 +75,19 @@
-

- {print.name} -

+
+

+ {print.name} +

+ {#if print.stl_file} + + + + {/if} +