Compare commits
2 Commits
ee6712cc8e
...
dfb73c74da
| Author | SHA1 | Date | |
|---|---|---|---|
| dfb73c74da | |||
| 16be4fdeaa |
13
Dockerfile
13
Dockerfile
@@ -16,7 +16,7 @@ COPY . .
|
|||||||
RUN bun run build
|
RUN bun run build
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM oven/bun:1-slim AS production
|
FROM oven/bun:1 AS production
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -43,16 +43,5 @@ ENV PORT=3000
|
|||||||
# Expose the port
|
# Expose the port
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
# Create a non-root user
|
|
||||||
RUN addgroup --system --gid 1001 nodejs && \
|
|
||||||
adduser --system --uid 1001 filaprint && \
|
|
||||||
chown -R filaprint:nodejs /app
|
|
||||||
|
|
||||||
USER filaprint
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
||||||
CMD curl -f http://localhost:3000/ || exit 1
|
|
||||||
|
|
||||||
# Start the application
|
# Start the application
|
||||||
CMD ["bun", "run", "start"]
|
CMD ["bun", "run", "start"]
|
||||||
|
|||||||
144
README.md
144
README.md
@@ -1,6 +1,8 @@
|
|||||||
# Filaprint
|
# Filaprint
|
||||||
|
|
||||||
Filaprint is a modern, premium web application designed to help 3D printing enthusiasts manage their filament inventory, track print jobs, and calculate costs and energy usage.
|
Filaprint is a modern, premium web application designed to help 3D printing enthusiasts manage their filament inventory, track print jobs, view 3D models, and calculate costs and energy usage.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 🛠️ Technology Stack
|
## 🛠️ Technology Stack
|
||||||
|
|
||||||
@@ -9,10 +11,12 @@ Filaprint is a modern, premium web application designed to help 3D printing enth
|
|||||||
- **Styling:** Tailwind CSS v4 (Cerberus Theme)
|
- **Styling:** Tailwind CSS v4 (Cerberus Theme)
|
||||||
- **State Management:** Svelte 5 Runes
|
- **State Management:** Svelte 5 Runes
|
||||||
- **Build Tool:** Vite
|
- **Build Tool:** Vite
|
||||||
|
- **3D Rendering:** Three.js (STL & OBJ loaders)
|
||||||
- **Data Visualization:** Chart.js
|
- **Data Visualization:** Chart.js
|
||||||
- **Icons:** Iconify (@iconify/svelte)
|
- **Icons:** Iconify (@iconify/svelte)
|
||||||
- **Database:** MongoDB with Mongoose
|
- **Database:** MongoDB with Mongoose
|
||||||
- **Authentication:** JWT with bcrypt password hashing
|
- **Authentication:** JWT with bcrypt password hashing
|
||||||
|
- **Container:** Docker with Docker Compose
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
@@ -37,23 +41,39 @@ Filaprint is a modern, premium web application designed to help 3D printing enth
|
|||||||
|
|
||||||
- **Log Prints:**
|
- **Log Prints:**
|
||||||
- Link to specific Printer and Filament Spool.
|
- Link to specific Printer and Filament Spool.
|
||||||
- Duration (minutes) and Weight used (g).
|
- Duration input with hours and minutes fields.
|
||||||
- Calculated Cost (auto-calculated or manual override).
|
- Weight used (g) and calculated cost (auto or manual).
|
||||||
- Status: Success, Fail, Cancelled, **In Progress**.
|
- Status: Success, Fail, Cancelled, **In Progress**.
|
||||||
|
- **3D Model Upload:** Attach STL or OBJ files to prints.
|
||||||
- **In Progress Tracking:**
|
- **In Progress Tracking:**
|
||||||
- Assign printer and spool to active jobs.
|
- Assign printer and spool to active jobs.
|
||||||
- Specify elapsed time for accurate dashboard countdown.
|
- Specify elapsed time for accurate dashboard countdown.
|
||||||
- Real-time progress display on dashboard.
|
- Real-time progress display on dashboard.
|
||||||
|
- **Cost Calculation:**
|
||||||
|
- Filament cost based on spool price and weight used.
|
||||||
|
- Electricity cost based on printer power consumption and duration.
|
||||||
|
- User-configurable electricity rate ($/kWh).
|
||||||
- **Edit/Delete:** Full CRUD operations for print history.
|
- **Edit/Delete:** Full CRUD operations for print history.
|
||||||
- **History:** Clickable entries with detailed information.
|
- **History:** Clickable entries with detailed information.
|
||||||
|
|
||||||
### 4. Printer Configuration
|
### 4. 3D Model Library
|
||||||
|
|
||||||
|
- **Model Gallery:** Browse all uploaded 3D models in a grid layout.
|
||||||
|
- **Interactive 3D Viewer:**
|
||||||
|
- Support for STL and OBJ file formats.
|
||||||
|
- Orbit controls (rotate, pan, zoom).
|
||||||
|
- Touch support for mobile devices.
|
||||||
|
- Auto-rotation with stop on interaction.
|
||||||
|
- **Upload Progress:** Progress bar with percentage for model uploads.
|
||||||
|
- **Full-Screen View:** Click to view models in an immersive full-screen viewer.
|
||||||
|
|
||||||
|
### 5. Printer Configuration
|
||||||
|
|
||||||
- **Profiles:** Manage multiple printers with custom names.
|
- **Profiles:** Manage multiple printers with custom names.
|
||||||
- **Specs:** Model name, Power consumption (Watts), Nozzle diameter (mm).
|
- **Specs:** Model name, Power consumption (Watts), Nozzle diameter (mm).
|
||||||
- **Configure Button:** Edit or delete printer profiles.
|
- **Configure Button:** Edit or delete printer profiles.
|
||||||
|
|
||||||
### 5. Analytics
|
### 6. Analytics
|
||||||
|
|
||||||
- **Daily Filament Usage:** Line chart showing filament consumption over time.
|
- **Daily Filament Usage:** Line chart showing filament consumption over time.
|
||||||
- **Daily Electricity Usage:** Bar chart showing power consumption in kWh.
|
- **Daily Electricity Usage:** Bar chart showing power consumption in kWh.
|
||||||
@@ -61,10 +81,13 @@ Filaprint is a modern, premium web application designed to help 3D printing enth
|
|||||||
- **Material Distribution:** Doughnut chart showing material breakdown.
|
- **Material Distribution:** Doughnut chart showing material breakdown.
|
||||||
- **Stats Summary:** Total prints, success rate, total electricity used.
|
- **Stats Summary:** Total prints, success rate, total electricity used.
|
||||||
|
|
||||||
### 6. User Management
|
### 7. User Management
|
||||||
|
|
||||||
- **Authentication:** Secure login/registration with JWT tokens.
|
- **Authentication:** Secure login/registration with JWT tokens.
|
||||||
- **User Settings:** Profile editing and password change.
|
- **User Settings:**
|
||||||
|
- Profile editing (username, location).
|
||||||
|
- Electricity rate configuration ($/kWh).
|
||||||
|
- Password change.
|
||||||
- **Admin Panel:** Manage users (Admin role only).
|
- **Admin Panel:** Manage users (Admin role only).
|
||||||
- **Role-Based Access:** Admin and User roles with appropriate permissions.
|
- **Role-Based Access:** Admin and User roles with appropriate permissions.
|
||||||
|
|
||||||
@@ -74,9 +97,11 @@ Filaprint is a modern, premium web application designed to help 3D printing enth
|
|||||||
|
|
||||||
- `_id`: ObjectId
|
- `_id`: ObjectId
|
||||||
- `username`: String (Required, Unique)
|
- `username`: String (Required, Unique)
|
||||||
- `email`: String
|
|
||||||
- `password`: String (Hashed with bcrypt)
|
- `password`: String (Hashed with bcrypt)
|
||||||
- `role`: String (Enum: User, Admin)
|
- `role`: String (Enum: User, Admin)
|
||||||
|
- `location`: String
|
||||||
|
- `electricity_rate`: Number (Default: 0.12 $/kWh)
|
||||||
|
- `currency`: String (Default: USD)
|
||||||
- `createdAt`: Date
|
- `createdAt`: Date
|
||||||
|
|
||||||
### Spool Schema
|
### Spool Schema
|
||||||
@@ -110,9 +135,11 @@ Filaprint is a modern, premium web application designed to help 3D printing enth
|
|||||||
- `name`: String
|
- `name`: String
|
||||||
- `duration_minutes`: Number
|
- `duration_minutes`: Number
|
||||||
- `filament_used_g`: Number
|
- `filament_used_g`: Number
|
||||||
- `calculated_cost_filament`: Number
|
- `calculated_cost_filament`: Number (Total cost including electricity)
|
||||||
|
- `calculated_cost_energy`: Number (Electricity cost only)
|
||||||
- `status`: String (Enum: Success, Fail, Cancelled, In Progress)
|
- `status`: String (Enum: Success, Fail, Cancelled, In Progress)
|
||||||
- `started_at`: Date (For In Progress jobs)
|
- `started_at`: Date (For In Progress jobs)
|
||||||
|
- `stl_file`: String (Path to uploaded 3D model)
|
||||||
- `date`: Date (Default: Date.now)
|
- `date`: Date (Default: Date.now)
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## 🚀 Getting Started
|
||||||
@@ -121,8 +148,9 @@ Filaprint is a modern, premium web application designed to help 3D printing enth
|
|||||||
|
|
||||||
- Node.js 18+ or Bun
|
- Node.js 18+ or Bun
|
||||||
- MongoDB instance (local or Atlas)
|
- MongoDB instance (local or Atlas)
|
||||||
|
- Docker (optional, for containerized deployment)
|
||||||
|
|
||||||
### Installation
|
### Local Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
# Clone the repository
|
||||||
@@ -138,36 +166,79 @@ cp .env.example .env
|
|||||||
|
|
||||||
# Run development server
|
# Run development server
|
||||||
bun run dev
|
bun run dev
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
# Start production server
|
||||||
|
bun run start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy environment file
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with secure values
|
||||||
|
|
||||||
|
# Build and start containers
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose logs -f filaprint
|
||||||
|
|
||||||
|
# Stop containers
|
||||||
|
docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
```env
|
```env
|
||||||
|
# MongoDB Connection
|
||||||
MONGODB_URI=mongodb://localhost:27017/filaprint
|
MONGODB_URI=mongodb://localhost:27017/filaprint
|
||||||
|
|
||||||
|
# JWT Secret (use a secure random string in production)
|
||||||
JWT_SECRET=your-super-secret-jwt-key
|
JWT_SECRET=your-super-secret-jwt-key
|
||||||
|
|
||||||
|
# Application Origin (required for CSRF protection)
|
||||||
|
ORIGIN=http://localhost:3000
|
||||||
|
|
||||||
|
# Docker MongoDB Settings
|
||||||
|
MONGO_USER=admin
|
||||||
|
MONGO_PASSWORD=changeme
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📁 Project Structure
|
## 📁 Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
filaprint/
|
||||||
├── lib/
|
├── src/
|
||||||
│ ├── components/
|
│ ├── lib/
|
||||||
│ │ ├── ui/ # Base UI components (Button, Card, Input, Modal)
|
│ │ ├── components/
|
||||||
│ │ ├── prints/ # Print-specific components (LogPrintModal, EditPrintModal)
|
│ │ │ ├── ui/ # Base UI components (Button, Card, Input, Modal)
|
||||||
│ │ └── Navbar.svelte
|
│ │ │ ├── prints/ # Print-specific components (LogPrintModal, EditPrintModal)
|
||||||
│ ├── models/ # Mongoose schemas
|
│ │ │ ├── STLViewer.svelte # 3D model viewer (STL & OBJ)
|
||||||
│ └── server/ # Server utilities (db connection, auth)
|
│ │ │ └── Navbar.svelte
|
||||||
├── routes/
|
│ │ ├── models/ # Mongoose schemas
|
||||||
│ ├── admin/users/ # Admin user management
|
│ │ └── server/ # Server utilities (db connection, auth)
|
||||||
│ ├── analytics/ # Analytics dashboard
|
│ ├── routes/
|
||||||
│ ├── login/ # Authentication
|
│ │ ├── admin/users/ # Admin user management
|
||||||
│ ├── printers/ # Printer management
|
│ │ ├── analytics/ # Analytics dashboard
|
||||||
│ ├── prints/ # Print job logging
|
│ │ ├── api/upload-stl/ # 3D model upload endpoint
|
||||||
│ ├── register/ # User registration
|
│ │ ├── library/ # 3D model library
|
||||||
│ ├── settings/ # User settings
|
│ │ ├── login/ # Authentication
|
||||||
│ └── spools/ # Filament inventory
|
│ │ ├── printers/ # Printer management
|
||||||
└── app.css # Global styles (Cerberus theme)
|
│ │ ├── prints/ # Print job logging
|
||||||
|
│ │ ├── register/ # User registration
|
||||||
|
│ │ ├── settings/ # User settings
|
||||||
|
│ │ └── spools/ # Filament inventory
|
||||||
|
│ └── app.css # Global styles (Cerberus theme)
|
||||||
|
├── static/
|
||||||
|
│ └── uploads/models/ # Uploaded 3D model files
|
||||||
|
├── server/ # Production server
|
||||||
|
├── Dockerfile # Container build instructions
|
||||||
|
├── docker-compose.yml # Container orchestration
|
||||||
|
└── package.json
|
||||||
```
|
```
|
||||||
|
|
||||||
## ✅ Completed Features
|
## ✅ Completed Features
|
||||||
@@ -177,18 +248,23 @@ src/
|
|||||||
- [x] Spool management (CRUD)
|
- [x] Spool management (CRUD)
|
||||||
- [x] Printer management (CRUD)
|
- [x] Printer management (CRUD)
|
||||||
- [x] Print job logging with "In Progress" support
|
- [x] Print job logging with "In Progress" support
|
||||||
- [x] Cost calculation (auto and manual)
|
- [x] Duration input with hours/minutes fields
|
||||||
|
- [x] Cost calculation (filament + electricity)
|
||||||
|
- [x] User-configurable electricity rate
|
||||||
- [x] Filament deduction on print completion
|
- [x] Filament deduction on print completion
|
||||||
- [x] Analytics with Chart.js (filament, electricity, materials)
|
- [x] Analytics with Chart.js (filament, electricity, materials)
|
||||||
- [x] User settings (profile, password change)
|
- [x] 3D Model Library with interactive viewer
|
||||||
|
- [x] STL and OBJ file upload with progress bar
|
||||||
|
- [x] Mobile hamburger menu (solid background)
|
||||||
|
- [x] User settings (profile, location, electricity rate, password)
|
||||||
- [x] Admin user management panel
|
- [x] Admin user management panel
|
||||||
- [x] Browser notifications for completed prints
|
- [x] Browser notifications for completed prints
|
||||||
- [x] Iconify icon library integration
|
- [x] Iconify icon library integration
|
||||||
- [x] Responsive design
|
- [x] Responsive design
|
||||||
|
- [x] Docker containerization
|
||||||
|
|
||||||
## 🔮 Future Enhancements
|
## 🔮 Future Enhancements
|
||||||
|
|
||||||
- [ ] 3D spool visualization with Threlte
|
|
||||||
- [ ] QR/Barcode scanning for quick spool lookup
|
- [ ] QR/Barcode scanning for quick spool lookup
|
||||||
- [ ] Photo uploads for print jobs
|
- [ ] Photo uploads for print jobs
|
||||||
- [ ] Export data (CSV/PDF reports)
|
- [ ] Export data (CSV/PDF reports)
|
||||||
@@ -196,3 +272,9 @@ src/
|
|||||||
- [ ] Dark/Light theme toggle
|
- [ ] Dark/Light theme toggle
|
||||||
- [ ] Email notifications
|
- [ ] Email notifications
|
||||||
- [ ] Print job templates
|
- [ ] Print job templates
|
||||||
|
- [ ] 3D printer integration (OctoPrint, Klipper)
|
||||||
|
- [ ] Thumbnail generation for 3D models
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
MIT License - See LICENSE file for details.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js";
|
import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js";
|
||||||
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
|
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
|
||||||
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -95,6 +96,8 @@
|
|||||||
|
|
||||||
if (extension === "obj") {
|
if (extension === "obj") {
|
||||||
loadOBJ();
|
loadOBJ();
|
||||||
|
} else if (extension === "gltf" || extension === "glb") {
|
||||||
|
loadGLTF();
|
||||||
} else {
|
} else {
|
||||||
loadSTL();
|
loadSTL();
|
||||||
}
|
}
|
||||||
@@ -161,6 +164,56 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadGLTF() {
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
|
||||||
|
loader.load(
|
||||||
|
modelPath,
|
||||||
|
(gltf) => {
|
||||||
|
const model = gltf.scene;
|
||||||
|
|
||||||
|
// Center the object
|
||||||
|
const box = new THREE.Box3().setFromObject(model);
|
||||||
|
const center = box.getCenter(new THREE.Vector3());
|
||||||
|
model.position.sub(center);
|
||||||
|
|
||||||
|
// Scale to fit
|
||||||
|
const size = box.getSize(new THREE.Vector3());
|
||||||
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
|
const scale = 50 / maxDim;
|
||||||
|
model.scale.set(scale, scale, scale);
|
||||||
|
|
||||||
|
// glTF models may have their own materials, apply default if missing
|
||||||
|
model.traverse((child) => {
|
||||||
|
if (child instanceof THREE.Mesh) {
|
||||||
|
if (
|
||||||
|
!child.material ||
|
||||||
|
(child.material as THREE.Material).type ===
|
||||||
|
"MeshBasicMaterial"
|
||||||
|
) {
|
||||||
|
child.material = new THREE.MeshPhongMaterial({
|
||||||
|
color: 0x3b82f6,
|
||||||
|
specular: 0x111111,
|
||||||
|
shininess: 50,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scene.add(model);
|
||||||
|
|
||||||
|
// Position camera
|
||||||
|
const distance = maxDim * scale * 2.5;
|
||||||
|
camera.position.set(distance, distance, distance);
|
||||||
|
controls.update();
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
(err) => {
|
||||||
|
console.error("Error loading glTF:", err);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function addGeometryToScene(geometry: THREE.BufferGeometry) {
|
function addGeometryToScene(geometry: THREE.BufferGeometry) {
|
||||||
// Center the geometry
|
// Center the geometry
|
||||||
geometry.computeBoundingBox();
|
geometry.computeBoundingBox();
|
||||||
|
|||||||
@@ -101,8 +101,12 @@
|
|||||||
stlFile = input.files[0];
|
stlFile = input.files[0];
|
||||||
uploadStatus = stlFile.name;
|
uploadStatus = stlFile.name;
|
||||||
uploadProgress = 0;
|
uploadProgress = 0;
|
||||||
|
removeModel = false; // Reset remove flag if selecting new file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track if user wants to remove the model
|
||||||
|
let removeModel = $state(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal title="Edit Print Log" {open} onclose={handleClose}>
|
<Modal title="Edit Print Log" {open} onclose={handleClose}>
|
||||||
@@ -121,6 +125,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle model removal
|
||||||
|
if (removeModel) {
|
||||||
|
formData.set("remove_model", "true");
|
||||||
|
}
|
||||||
|
|
||||||
// Convert hours + minutes to total minutes
|
// Convert hours + minutes to total minutes
|
||||||
const hours = Number(formData.get("duration_hours") || 0);
|
const hours = Number(formData.get("duration_hours") || 0);
|
||||||
const mins = Number(formData.get("duration_mins") || 0);
|
const mins = Number(formData.get("duration_mins") || 0);
|
||||||
@@ -222,22 +231,53 @@
|
|||||||
3D Model {print.stl_file ? "" : "(Optional)"}
|
3D Model {print.stl_file ? "" : "(Optional)"}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{#if print.stl_file && browser && !stlFile}
|
{#if print.stl_file && browser && !stlFile && !removeModel}
|
||||||
<!-- Show existing STL viewer -->
|
<!-- Show existing STL viewer -->
|
||||||
<div
|
<div class="relative">
|
||||||
class="flex justify-center bg-slate-900 rounded-lg p-2"
|
<div
|
||||||
>
|
class="flex justify-center bg-slate-900 rounded-lg p-2"
|
||||||
{#await import("$lib/components/STLViewer.svelte") then { default: STLViewer }}
|
>
|
||||||
<STLViewer
|
{#await import("$lib/components/STLViewer.svelte") then { default: STLViewer }}
|
||||||
modelPath={print.stl_file}
|
<STLViewer
|
||||||
width={400}
|
modelPath={print.stl_file}
|
||||||
height={250}
|
width={400}
|
||||||
/>
|
height={250}
|
||||||
{/await}
|
/>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
<!-- Remove button overlay -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-4 right-4 p-2 bg-red-500/80 hover:bg-red-500 text-white rounded-lg transition-colors"
|
||||||
|
onclick={() => (removeModel = true)}
|
||||||
|
title="Remove 3D model"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:delete" class="w-5 h-5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-slate-500 text-center">
|
<p class="text-xs text-slate-500 text-center">
|
||||||
Click below to replace with a new model
|
Click below to replace with a new model
|
||||||
</p>
|
</p>
|
||||||
|
{:else if removeModel && print.stl_file}
|
||||||
|
<!-- Removal confirmation -->
|
||||||
|
<div
|
||||||
|
class="p-4 rounded-lg bg-red-500/10 border border-red-500/30 text-center"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon="mdi:file-remove"
|
||||||
|
class="w-10 h-10 text-red-400 mx-auto mb-2"
|
||||||
|
/>
|
||||||
|
<p class="text-sm text-red-300 mb-3">
|
||||||
|
Model will be removed when you save
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-xs text-slate-400 hover:text-white underline"
|
||||||
|
onclick={() => (removeModel = false)}
|
||||||
|
>
|
||||||
|
Cancel removal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Upload button or progress -->
|
<!-- Upload button or progress -->
|
||||||
@@ -280,7 +320,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".stl,.obj"
|
accept=".stl,.obj,.gltf,.glb"
|
||||||
class="sr-only"
|
class="sr-only"
|
||||||
onchange={handleFileSelect}
|
onchange={handleFileSelect}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -239,7 +239,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".stl,.obj"
|
accept=".stl,.obj,.gltf,.glb"
|
||||||
class="sr-only"
|
class="sr-only"
|
||||||
onchange={handleFileSelect}
|
onchange={handleFileSelect}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { existsSync } from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const UPLOAD_DIR = 'static/uploads/models';
|
const UPLOAD_DIR = 'static/uploads/models';
|
||||||
const ALLOWED_EXTENSIONS = ['.stl', '.obj'];
|
const ALLOWED_EXTENSIONS = ['.stl', '.obj', '.gltf', '.glb'];
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
if (!locals.user) {
|
if (!locals.user) {
|
||||||
@@ -24,7 +24,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||||||
const extension = '.' + fileName.split('.').pop();
|
const extension = '.' + fileName.split('.').pop();
|
||||||
|
|
||||||
if (!ALLOWED_EXTENSIONS.includes(extension)) {
|
if (!ALLOWED_EXTENSIONS.includes(extension)) {
|
||||||
throw error(400, 'Only STL and OBJ files are allowed');
|
throw error(400, 'Only STL, OBJ, GLTF, and GLB files are allowed');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create upload directory if it doesn't exist
|
// Create upload directory if it doesn't exist
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import { User } from '$lib/models/User';
|
|||||||
import { connectDB } from '$lib/server/db';
|
import { connectDB } from '$lib/server/db';
|
||||||
import type { PageServerLoad, Actions } from './$types';
|
import type { PageServerLoad, Actions } from './$types';
|
||||||
import { fail, redirect } from '@sveltejs/kit';
|
import { fail, redirect } from '@sveltejs/kit';
|
||||||
|
import { unlink } from 'fs/promises';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ locals }) => {
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
if (!locals.user) throw redirect(303, '/login');
|
if (!locals.user) throw redirect(303, '/login');
|
||||||
@@ -142,6 +145,7 @@ export const actions: Actions = {
|
|||||||
const printer_id = formData.get('printer_id');
|
const printer_id = formData.get('printer_id');
|
||||||
const spool_id = formData.get('spool_id');
|
const spool_id = formData.get('spool_id');
|
||||||
const stl_file = formData.get('stl_file');
|
const stl_file = formData.get('stl_file');
|
||||||
|
const remove_model = formData.get('remove_model');
|
||||||
|
|
||||||
if (!id || !name) {
|
if (!id || !name) {
|
||||||
return fail(400, { missing: true });
|
return fail(400, { missing: true });
|
||||||
@@ -218,9 +222,31 @@ export const actions: Actions = {
|
|||||||
if (spool_id) {
|
if (spool_id) {
|
||||||
updateData.spool_id = spool_id;
|
updateData.spool_id = spool_id;
|
||||||
}
|
}
|
||||||
// Update STL file if provided
|
// Handle STL file: update if new one provided, or remove if requested
|
||||||
if (stl_file) {
|
if (stl_file) {
|
||||||
|
// If replacing an existing model, delete the old file
|
||||||
|
if (printJob.stl_file) {
|
||||||
|
const oldFilePath = path.join('static', printJob.stl_file);
|
||||||
|
if (existsSync(oldFilePath)) {
|
||||||
|
try {
|
||||||
|
await unlink(oldFilePath);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to delete old model file:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
updateData.stl_file = stl_file;
|
updateData.stl_file = stl_file;
|
||||||
|
} else if (remove_model === 'true' && printJob.stl_file) {
|
||||||
|
// Delete the file from disk
|
||||||
|
const filePath = path.join('static', printJob.stl_file);
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
await unlink(filePath);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to delete model file:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateData.stl_file = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await PrintJob.findOneAndUpdate(
|
await PrintJob.findOneAndUpdate(
|
||||||
@@ -246,6 +272,21 @@ export const actions: Actions = {
|
|||||||
await connectDB();
|
await connectDB();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Find the print first to get the model file path
|
||||||
|
const printJob = await PrintJob.findOne({ _id: id, user_id: locals.user.id });
|
||||||
|
|
||||||
|
if (printJob?.stl_file) {
|
||||||
|
// Delete the model file from disk
|
||||||
|
const filePath = path.join('static', printJob.stl_file);
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
await unlink(filePath);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to delete model file:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await PrintJob.findOneAndDelete({ _id: id, user_id: locals.user.id });
|
await PrintJob.findOneAndDelete({ _id: id, user_id: locals.user.id });
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user