mirror of
https://github.com/RunLit/Bambu-Run.git
synced 2026-06-22 22:19:03 +01:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c56711c57 |
@@ -52,7 +52,7 @@ class FilamentForm(forms.ModelForm):
|
|||||||
model = Filament
|
model = Filament
|
||||||
fields = [
|
fields = [
|
||||||
'tray_uuid', 'tag_uid', 'tag_id', 'created_by',
|
'tray_uuid', 'tag_uid', 'tag_id', 'created_by',
|
||||||
'filament_type', 'type', 'sub_type', 'brand', 'color', 'color_hex',
|
'filament_type', 'type', 'sub_type', 'brand', 'color', 'color_hex', 'is_transparent',
|
||||||
'diameter', 'initial_weight_grams',
|
'diameter', 'initial_weight_grams',
|
||||||
'remaining_percent', 'remaining_weight_grams',
|
'remaining_percent', 'remaining_weight_grams',
|
||||||
'is_loaded_in_ams', 'current_tray_id',
|
'is_loaded_in_ams', 'current_tray_id',
|
||||||
@@ -71,10 +71,10 @@ class FilamentForm(forms.ModelForm):
|
|||||||
}),
|
}),
|
||||||
'tag_id': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Optional - User-defined ID'}),
|
'tag_id': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Optional - User-defined ID'}),
|
||||||
'created_by': forms.Select(attrs={'class': 'form-select'}),
|
'created_by': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'filament_type': forms.Select(attrs={'class': 'form-select'}),
|
'filament_type': forms.Select(attrs={'class': 'form-select', 'id': 'id_filament_type'}),
|
||||||
'type': forms.HiddenInput(),
|
'type': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., PLA, PETG, ABS'}),
|
||||||
'sub_type': forms.HiddenInput(),
|
'sub_type': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., PLA Basic (optional)'}),
|
||||||
'brand': forms.HiddenInput(),
|
'brand': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., Bambu Lab'}),
|
||||||
'color': forms.Select(attrs={'class': 'form-select', 'id': 'id_color'}),
|
'color': forms.Select(attrs={'class': 'form-select', 'id': 'id_color'}),
|
||||||
'color_hex': forms.TextInput(attrs={
|
'color_hex': forms.TextInput(attrs={
|
||||||
'class': 'form-control',
|
'class': 'form-control',
|
||||||
@@ -85,6 +85,7 @@ class FilamentForm(forms.ModelForm):
|
|||||||
'initial_weight_grams': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': '1000'}),
|
'initial_weight_grams': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': '1000'}),
|
||||||
'remaining_percent': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '100'}),
|
'remaining_percent': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '100'}),
|
||||||
'remaining_weight_grams': forms.NumberInput(attrs={'class': 'form-control', 'readonly': 'readonly'}),
|
'remaining_weight_grams': forms.NumberInput(attrs={'class': 'form-control', 'readonly': 'readonly'}),
|
||||||
|
'is_transparent': forms.CheckboxInput(attrs={'class': 'form-check-input', 'id': 'id_is_transparent'}),
|
||||||
'is_loaded_in_ams': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
'is_loaded_in_ams': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||||
'current_tray_id': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '3'}),
|
'current_tray_id': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '3'}),
|
||||||
'purchase_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
'purchase_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
def _auto_create_filament(self, tray_data):
|
def _auto_create_filament(self, tray_data):
|
||||||
from bambu_run.models import Filament, FilamentType
|
from bambu_run.models import Filament, FilamentType
|
||||||
from bambu_run.utils import strip_color_padding, match_filament_color
|
from bambu_run.utils import strip_color_padding, match_filament_color, is_mqtt_color_transparent
|
||||||
|
|
||||||
tray_uuid = tray_data.get('tray_uuid')
|
tray_uuid = tray_data.get('tray_uuid')
|
||||||
tag_uid = tray_data.get('tag_uid')
|
tag_uid = tray_data.get('tag_uid')
|
||||||
@@ -329,6 +329,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
default_brand = app_settings.AUTO_CREATE_BRAND
|
default_brand = app_settings.AUTO_CREATE_BRAND
|
||||||
|
|
||||||
|
transparent = is_mqtt_color_transparent(mqtt_color)
|
||||||
color_code = strip_color_padding(mqtt_color)
|
color_code = strip_color_padding(mqtt_color)
|
||||||
color_hex = f"#{color_code}" if color_code else None
|
color_hex = f"#{color_code}" if color_code else None
|
||||||
|
|
||||||
@@ -341,6 +342,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
if filament_color:
|
if filament_color:
|
||||||
color_name = filament_color.color_name
|
color_name = filament_color.color_name
|
||||||
|
transparent = transparent or filament_color.is_transparent
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
logger.info(f"Matched color from database: {color_name} (#{color_code})")
|
logger.info(f"Matched color from database: {color_name} (#{color_code})")
|
||||||
else:
|
else:
|
||||||
@@ -368,6 +370,7 @@ class Command(BaseCommand):
|
|||||||
brand=default_brand,
|
brand=default_brand,
|
||||||
color=color_name,
|
color=color_name,
|
||||||
color_hex=color_hex,
|
color_hex=color_hex,
|
||||||
|
is_transparent=transparent,
|
||||||
diameter=diameter,
|
diameter=diameter,
|
||||||
initial_weight_grams=initial_weight,
|
initial_weight_grams=initial_weight,
|
||||||
remaining_percent=remain_percent,
|
remaining_percent=remain_percent,
|
||||||
|
|||||||
@@ -371,6 +371,11 @@ class Command(BaseCommand):
|
|||||||
return "created"
|
return "created"
|
||||||
return "no_type"
|
return "no_type"
|
||||||
|
|
||||||
|
# ── Transparent detection ────────────────────────────────────────────
|
||||||
|
# "Translucent" (no colour qualifier) + #000000 = clear/transparent filament.
|
||||||
|
# Bambu Lab AMS reports these as 00000000 (alpha=00).
|
||||||
|
is_transparent = color_name.strip().lower() == "translucent" and hex_code == "000000"
|
||||||
|
|
||||||
# ── Duplicate check ──────────────────────────────────────────────────
|
# ── Duplicate check ──────────────────────────────────────────────────
|
||||||
# All five fields must match to be considered a duplicate:
|
# All five fields must match to be considered a duplicate:
|
||||||
# color_code (exact), color_name (case-insensitive), brand,
|
# color_code (exact), color_name (case-insensitive), brand,
|
||||||
@@ -388,9 +393,10 @@ class Command(BaseCommand):
|
|||||||
return "duplicate"
|
return "duplicate"
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
|
transparent_note = " [transparent]" if is_transparent else ""
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
f" [dry-run] Would create: {color_name!r} #{hex_code} "
|
f" [dry-run] Would create: {color_name!r} #{hex_code} "
|
||||||
f"({filament_type} / {filament_sub_type})"
|
f"({filament_type} / {filament_sub_type}){transparent_note}"
|
||||||
)
|
)
|
||||||
return "created"
|
return "created"
|
||||||
|
|
||||||
@@ -404,6 +410,7 @@ class Command(BaseCommand):
|
|||||||
filament_type=filament_type,
|
filament_type=filament_type,
|
||||||
filament_sub_type=filament_sub_type,
|
filament_sub_type=filament_sub_type,
|
||||||
brand=BRAND,
|
brand=BRAND,
|
||||||
|
is_transparent=is_transparent,
|
||||||
)
|
)
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
f" + {color_name!r} #{hex_code} ({filament_type} / {filament_sub_type})"
|
f" + {color_name!r} #{hex_code} ({filament_type} / {filament_sub_type})"
|
||||||
|
|||||||
27
bambu_run/migrations/0002_filament_is_transparent.py
Normal file
27
bambu_run/migrations/0002_filament_is_transparent.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bambu_run", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="filamentcolor",
|
||||||
|
name="is_transparent",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="True for clear/transparent filaments — display as checkerboard, not solid color",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="filament",
|
||||||
|
name="is_transparent",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="True for clear/transparent filaments — display as checkerboard, not solid color",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -259,6 +259,10 @@ class FilamentColor(models.Model):
|
|||||||
default='Bambu Lab',
|
default='Bambu Lab',
|
||||||
help_text="Manufacturer name"
|
help_text="Manufacturer name"
|
||||||
)
|
)
|
||||||
|
is_transparent = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="True for clear/transparent filaments — display as checkerboard, not solid color"
|
||||||
|
)
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
@@ -329,6 +333,10 @@ class Filament(models.Model):
|
|||||||
max_length=7, null=True, blank=True,
|
max_length=7, null=True, blank=True,
|
||||||
help_text="Color hex code for display (#RRGGBB)"
|
help_text="Color hex code for display (#RRGGBB)"
|
||||||
)
|
)
|
||||||
|
is_transparent = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="True for clear/transparent filaments — display as checkerboard, not solid color"
|
||||||
|
)
|
||||||
|
|
||||||
# Physical properties
|
# Physical properties
|
||||||
diameter = models.DecimalField(
|
diameter = models.DecimalField(
|
||||||
|
|||||||
156
bambu_run/static/bambu_run/js/filament_form.js
Normal file
156
bambu_run/static/bambu_run/js/filament_form.js
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* filament_form.js — Filament add/edit form interactions.
|
||||||
|
*
|
||||||
|
* Handles:
|
||||||
|
* - Filament type preset → auto-fill Type / Sub Type / Brand
|
||||||
|
* - Transparent checkbox → toggle color picker vs. checkerboard swatch
|
||||||
|
* - Color picker ↔ hex text sync
|
||||||
|
* - Delete confirmation modal
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
|
// ── Filament type preset auto-fill ────────────────────────────────────────
|
||||||
|
|
||||||
|
const dataEl = document.getElementById('filament-type-data');
|
||||||
|
const filamentTypeMap = dataEl ? JSON.parse(dataEl.textContent) : {};
|
||||||
|
|
||||||
|
const filamentTypeSelect = document.getElementById('id_filament_type');
|
||||||
|
const typeField = document.getElementById('id_type');
|
||||||
|
const subTypeField = document.getElementById('id_sub_type');
|
||||||
|
const brandField = document.getElementById('id_brand');
|
||||||
|
|
||||||
|
if (filamentTypeSelect) {
|
||||||
|
filamentTypeSelect.addEventListener('change', function () {
|
||||||
|
const mapping = filamentTypeMap[this.value];
|
||||||
|
if (mapping && typeField && subTypeField && brandField) {
|
||||||
|
typeField.value = mapping.type;
|
||||||
|
subTypeField.value = mapping.sub_type;
|
||||||
|
brandField.value = mapping.brand;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Transparent toggle ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const transparentCheckbox = document.getElementById('id_is_transparent');
|
||||||
|
const transparentSwatch = document.getElementById('transparent-swatch');
|
||||||
|
const colorPicker = document.getElementById('id_color_hex_picker');
|
||||||
|
const colorText = document.getElementById('id_color_hex_text');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show checkerboard swatch and disable color inputs when transparent,
|
||||||
|
* restore normal color picker when not transparent.
|
||||||
|
* @param {boolean} isTransparent
|
||||||
|
*/
|
||||||
|
function applyTransparentState(isTransparent) {
|
||||||
|
if (!colorPicker) return;
|
||||||
|
if (isTransparent) {
|
||||||
|
transparentSwatch.style.display = 'block';
|
||||||
|
colorPicker.style.display = 'none';
|
||||||
|
colorPicker.disabled = true;
|
||||||
|
if (colorText) { colorText.disabled = true; colorText.value = ''; }
|
||||||
|
} else {
|
||||||
|
transparentSwatch.style.display = 'none';
|
||||||
|
colorPicker.style.display = '';
|
||||||
|
colorPicker.disabled = false;
|
||||||
|
if (colorText) { colorText.disabled = false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transparentCheckbox) {
|
||||||
|
applyTransparentState(transparentCheckbox.checked);
|
||||||
|
transparentCheckbox.addEventListener('change', function () {
|
||||||
|
applyTransparentState(this.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Color picker ↔ hex text sync ──────────────────────────────────────────
|
||||||
|
|
||||||
|
if (colorPicker && colorText) {
|
||||||
|
colorPicker.addEventListener('input', function () {
|
||||||
|
colorText.value = this.value.toUpperCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
colorText.addEventListener('input', function () {
|
||||||
|
const value = this.value.trim();
|
||||||
|
if (/^#[0-9A-Fa-f]{6}$/.test(value)) {
|
||||||
|
colorPicker.value = value;
|
||||||
|
this.classList.remove('is-invalid');
|
||||||
|
} else if (value.length === 7) {
|
||||||
|
this.classList.add('is-invalid');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (colorText.value && /^#[0-9A-Fa-f]{6}$/.test(colorText.value)) {
|
||||||
|
colorPicker.value = colorText.value;
|
||||||
|
} else if (colorPicker.value && !colorText.value) {
|
||||||
|
colorText.value = colorPicker.value.toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Delete confirmation modal ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
const deleteConfirmText = document.getElementById('deleteConfirmText');
|
||||||
|
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
|
||||||
|
const deleteForm = document.getElementById('deleteForm');
|
||||||
|
const deleteModal = document.getElementById('deleteModal');
|
||||||
|
|
||||||
|
if (deleteConfirmText && confirmDeleteBtn) {
|
||||||
|
deleteConfirmText.addEventListener('input', function () {
|
||||||
|
const value = this.value.trim();
|
||||||
|
if (value === 'DELETE') {
|
||||||
|
confirmDeleteBtn.disabled = false;
|
||||||
|
this.classList.remove('is-invalid');
|
||||||
|
this.classList.add('is-valid');
|
||||||
|
} else {
|
||||||
|
confirmDeleteBtn.disabled = true;
|
||||||
|
this.classList.remove('is-valid');
|
||||||
|
if (value.length > 0) {
|
||||||
|
this.classList.add('is-invalid');
|
||||||
|
} else {
|
||||||
|
this.classList.remove('is-invalid');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deleteForm) {
|
||||||
|
deleteForm.addEventListener('submit', function (e) {
|
||||||
|
if (confirmDeleteBtn.disabled) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Please type DELETE to confirm deletion');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteModal) {
|
||||||
|
deleteModal.addEventListener('hidden.bs.modal', function () {
|
||||||
|
deleteConfirmText.value = '';
|
||||||
|
confirmDeleteBtn.disabled = true;
|
||||||
|
deleteConfirmText.classList.remove('is-valid', 'is-invalid');
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteModal.addEventListener('shown.bs.modal', function () {
|
||||||
|
deleteConfirmText.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Delete button modal opener (backup) ───────────────────────────────────
|
||||||
|
|
||||||
|
const deleteBtn = document.getElementById('deleteBtn');
|
||||||
|
if (deleteBtn && deleteModal) {
|
||||||
|
deleteBtn.addEventListener('click', function () {
|
||||||
|
if (!deleteModal.classList.contains('show')) {
|
||||||
|
if (typeof bootstrap !== 'undefined') {
|
||||||
|
bootstrap.Modal.getOrCreateInstance(deleteModal).show();
|
||||||
|
} else if (typeof coreui !== 'undefined' && coreui.Modal) {
|
||||||
|
coreui.Modal.getOrCreateInstance(deleteModal).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -646,8 +646,20 @@ function hexToRgba(hex, alpha) {
|
|||||||
function applyFilamentColors() {
|
function applyFilamentColors() {
|
||||||
// Apply colors to filament cards
|
// Apply colors to filament cards
|
||||||
document.querySelectorAll('.filament-card').forEach(card => {
|
document.querySelectorAll('.filament-card').forEach(card => {
|
||||||
|
const isTransparent = card.getAttribute('data-filament-transparent') === 'true';
|
||||||
const colorHex = card.getAttribute('data-filament-color');
|
const colorHex = card.getAttribute('data-filament-color');
|
||||||
if (colorHex) {
|
|
||||||
|
if (isTransparent) {
|
||||||
|
// Checkerboard left border and subtle background for clear filaments
|
||||||
|
card.style.borderLeft = '4px solid #aaa';
|
||||||
|
card.style.background = 'repeating-conic-gradient(rgba(180,180,180,0.15) 0% 25%, transparent 0% 50%) 0 0/10px 10px';
|
||||||
|
|
||||||
|
const badge = card.querySelector('.filament-badge');
|
||||||
|
if (badge) {
|
||||||
|
badge.style.backgroundColor = '#aaa';
|
||||||
|
badge.style.color = '#fff';
|
||||||
|
}
|
||||||
|
} else if (colorHex) {
|
||||||
const color = '#' + colorHex;
|
const color = '#' + colorHex;
|
||||||
|
|
||||||
// Set card background with gradient
|
// Set card background with gradient
|
||||||
|
|||||||
@@ -54,11 +54,19 @@
|
|||||||
{% for color in colors %}
|
{% for color in colors %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
|
{% if color.is_transparent %}
|
||||||
|
<div style="width: 50px; height: 50px; border-radius: 4px; border: 2px solid #ddd; background: repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 0 0/10px 10px;" title="Clear / Transparent"></div>
|
||||||
|
{% else %}
|
||||||
<div style="width: 50px; height: 50px; background-color: {{ color.get_hex_color }}; border-radius: 4px; border: 2px solid #ddd;"></div>
|
<div style="width: 50px; height: 50px; background-color: {{ color.get_hex_color }}; border-radius: 4px; border: 2px solid #ddd;"></div>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle"><strong>{{ color.color_name }}</strong></td>
|
<td class="align-middle"><strong>{{ color.color_name }}</strong></td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
|
{% if color.is_transparent %}
|
||||||
|
<span class="text-muted fst-italic">Clear / Transparent</span>
|
||||||
|
{% else %}
|
||||||
<span class="font-monospace">{{ color.get_hex_color }}</span>
|
<span class="font-monospace">{{ color.get_hex_color }}</span>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<span class="badge bg-secondary">{{ color.filament_type }}</span>
|
<span class="badge bg-secondary">{{ color.filament_type }}</span>
|
||||||
|
|||||||
@@ -27,10 +27,14 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6>Color</h6>
|
<h6>Color</h6>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
|
{% if filament.is_transparent %}
|
||||||
|
<div style="width: 50px; height: 50px; border-radius: 8px; margin-right: 15px; border: 2px solid #ddd; background: repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 0 0/10px 10px;" title="Clear / Transparent"></div>
|
||||||
|
{% else %}
|
||||||
<div style="width: 50px; height: 50px; background-color: {{ filament.color_hex|default:'#999' }}; border-radius: 8px; margin-right: 15px; border: 2px solid #ddd;"></div>
|
<div style="width: 50px; height: 50px; background-color: {{ filament.color_hex|default:'#999' }}; border-radius: 8px; margin-right: 15px; border: 2px solid #ddd;"></div>
|
||||||
|
{% endif %}
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ filament.color }}</strong><br>
|
<strong>{{ filament.color }}</strong><br>
|
||||||
<small class="text-muted">{{ filament.color_hex }}</small>
|
<small class="text-muted">{% if filament.is_transparent %}Clear / Transparent{% else %}{{ filament.color_hex }}{% endif %}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,6 +43,14 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<h5>Specifications</h5>
|
<h5>Specifications</h5>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label">Filament Type Preset</label>
|
||||||
|
{{ form.filament_type }}
|
||||||
|
<small class="form-text text-muted">Selecting a preset auto-fills Type, Sub Type, and Brand below.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Type *</label>
|
<label class="form-label">Type *</label>
|
||||||
@@ -62,12 +70,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3 align-items-end">
|
||||||
<div class="col-md-3">
|
<div class="col-md-2">
|
||||||
<label class="form-label">Color Picker</label>
|
<label class="form-label">Color Picker</label>
|
||||||
|
<div id="transparent-swatch" style="display:none; width:100%; height:38px; border-radius:4px; border:1px solid #ddd; background: repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 0 0/10px 10px;" title="Clear / Transparent"></div>
|
||||||
{{ form.color_hex }}
|
{{ form.color_hex }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-2">
|
||||||
|
<div class="form-check mt-4">
|
||||||
|
{{ form.is_transparent }}
|
||||||
|
<label class="form-check-label" for="id_is_transparent">Transparent / Clear</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
<label class="form-label">{{ form.color_hex_text.label }}</label>
|
<label class="form-label">{{ form.color_hex_text.label }}</label>
|
||||||
{{ form.color_hex_text }}
|
{{ form.color_hex_text }}
|
||||||
<small class="form-text text-muted">e.g. #0A2CA5</small>
|
<small class="form-text text-muted">e.g. #0A2CA5</small>
|
||||||
@@ -209,95 +224,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
{# Server-side data consumed by filament_form.js #}
|
||||||
// Sync color picker and text input
|
<script type="application/json" id="filament-type-data">{{ filament_type_map|safe }}</script>
|
||||||
const colorPicker = document.getElementById('id_color_hex_picker');
|
<script src="{% static 'bambu_run/js/filament_form.js' %}"></script>
|
||||||
const colorText = document.getElementById('id_color_hex_text');
|
|
||||||
|
|
||||||
if (colorPicker && colorText) {
|
|
||||||
colorPicker.addEventListener('input', function() {
|
|
||||||
colorText.value = this.value.toUpperCase();
|
|
||||||
});
|
|
||||||
|
|
||||||
colorText.addEventListener('input', function() {
|
|
||||||
const value = this.value.trim();
|
|
||||||
if (/^#[0-9A-Fa-f]{6}$/.test(value)) {
|
|
||||||
colorPicker.value = value;
|
|
||||||
this.classList.remove('is-invalid');
|
|
||||||
} else if (value.length === 7) {
|
|
||||||
this.classList.add('is-invalid');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (colorText.value && /^#[0-9A-Fa-f]{6}$/.test(colorText.value)) {
|
|
||||||
colorPicker.value = colorText.value;
|
|
||||||
} else if (colorPicker.value && !colorText.value) {
|
|
||||||
colorText.value = colorPicker.value.toUpperCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete confirmation logic
|
|
||||||
const deleteConfirmText = document.getElementById('deleteConfirmText');
|
|
||||||
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
|
|
||||||
const deleteForm = document.getElementById('deleteForm');
|
|
||||||
const deleteModal = document.getElementById('deleteModal');
|
|
||||||
|
|
||||||
if (deleteConfirmText && confirmDeleteBtn) {
|
|
||||||
deleteConfirmText.addEventListener('input', function() {
|
|
||||||
const value = this.value.trim();
|
|
||||||
if (value === 'DELETE') {
|
|
||||||
confirmDeleteBtn.disabled = false;
|
|
||||||
this.classList.remove('is-invalid');
|
|
||||||
this.classList.add('is-valid');
|
|
||||||
} else {
|
|
||||||
confirmDeleteBtn.disabled = true;
|
|
||||||
this.classList.remove('is-valid');
|
|
||||||
if (value.length > 0) {
|
|
||||||
this.classList.add('is-invalid');
|
|
||||||
} else {
|
|
||||||
this.classList.remove('is-invalid');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (deleteForm) {
|
|
||||||
deleteForm.addEventListener('submit', function(e) {
|
|
||||||
if (confirmDeleteBtn.disabled) {
|
|
||||||
e.preventDefault();
|
|
||||||
alert('Please type DELETE to confirm deletion');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deleteModal) {
|
|
||||||
deleteModal.addEventListener('hidden.bs.modal', function() {
|
|
||||||
deleteConfirmText.value = '';
|
|
||||||
confirmDeleteBtn.disabled = true;
|
|
||||||
deleteConfirmText.classList.remove('is-valid', 'is-invalid');
|
|
||||||
});
|
|
||||||
|
|
||||||
deleteModal.addEventListener('shown.bs.modal', function() {
|
|
||||||
deleteConfirmText.focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup modal opener
|
|
||||||
const deleteBtn = document.getElementById('deleteBtn');
|
|
||||||
if (deleteBtn && deleteModal) {
|
|
||||||
deleteBtn.addEventListener('click', function() {
|
|
||||||
if (!deleteModal.classList.contains('show')) {
|
|
||||||
if (typeof bootstrap !== 'undefined') {
|
|
||||||
const modalInstance = bootstrap.Modal.getOrCreateInstance(deleteModal);
|
|
||||||
modalInstance.show();
|
|
||||||
} else if (typeof coreui !== 'undefined' && coreui.Modal) {
|
|
||||||
const modalInstance = coreui.Modal.getOrCreateInstance(deleteModal);
|
|
||||||
modalInstance.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -122,7 +122,11 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
|
{% if filament.is_transparent %}
|
||||||
|
<div style="width: 30px; height: 30px; border-radius: 4px; margin-right: 10px; border: 1px solid #ddd; background: repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 0 0/10px 10px;" title="Clear / Transparent"></div>
|
||||||
|
{% else %}
|
||||||
<div style="width: 30px; height: 30px; background-color: {{ filament.color_hex|default:'#999' }}; border-radius: 4px; margin-right: 10px; border: 1px solid #ddd;"></div>
|
<div style="width: 30px; height: 30px; background-color: {{ filament.color_hex|default:'#999' }}; border-radius: 4px; margin-right: 10px; border: 1px solid #ddd;"></div>
|
||||||
|
{% endif %}
|
||||||
{{ filament.color }}
|
{{ filament.color }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -152,7 +152,7 @@
|
|||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
{% for filament in stats.filaments %}
|
{% for filament in stats.filaments %}
|
||||||
<div class="col-12 col-md-6 col-lg-3">
|
<div class="col-12 col-md-6 col-lg-3">
|
||||||
<div class="card filament-card" data-filament-color="{{ filament.color|slice:':6' }}">
|
<div class="card filament-card" data-filament-color="{{ filament.color|slice:':6' }}"{% if filament.is_transparent %} data-filament-transparent="true"{% endif %}>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<h6 class="mb-0">Tray {{ filament.tray_id }}</h6>
|
<h6 class="mb-0">Tray {{ filament.tray_id }}</h6>
|
||||||
|
|||||||
@@ -3,14 +3,23 @@ Utility functions for filament color matching
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# BambuLab AMS reports colors as 8-char hex with an alpha channel suffix (e.g. '489FDFFF').
|
# BambuLab AMS reports colors as 8-char hex with an alpha channel suffix (e.g. '489FDFFF').
|
||||||
# The last two chars are always 'FF' (fully opaque). Only the first 6 chars are the RGB value.
|
# Opaque filaments use alpha 'FF'. Clear/transparent filaments use alpha '00' (e.g. '00000000').
|
||||||
MQTT_COLOR_HEX_LENGTH = 6
|
MQTT_COLOR_HEX_LENGTH = 6
|
||||||
|
|
||||||
|
|
||||||
|
def is_mqtt_color_transparent(mqtt_color):
|
||||||
|
"""
|
||||||
|
Return True if the AMS color represents a clear/transparent filament.
|
||||||
|
Bambu Lab uses alpha=00 for transparent (e.g. '00000000'), not 'FF' like opaque filaments.
|
||||||
|
"""
|
||||||
|
return bool(mqtt_color) and len(mqtt_color) == 8 and mqtt_color[6:8].upper() == '00'
|
||||||
|
|
||||||
|
|
||||||
def strip_color_padding(mqtt_color):
|
def strip_color_padding(mqtt_color):
|
||||||
"""
|
"""
|
||||||
Strip FF padding from MQTT color
|
Strip alpha padding from MQTT color, returning the 6-char RGB hex.
|
||||||
MQTT: '000000FF' -> '000000'
|
MQTT: '000000FF' -> '000000' (opaque black)
|
||||||
|
MQTT: '00000000' -> '000000' (transparent — use is_mqtt_color_transparent() to distinguish)
|
||||||
MQTT: 'FF6A13FF' -> 'FF6A13'
|
MQTT: 'FF6A13FF' -> 'FF6A13'
|
||||||
"""
|
"""
|
||||||
if not mqtt_color:
|
if not mqtt_color:
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ class PrinterDashboardView(LoginRequiredMixin, TemplateView):
|
|||||||
if snapshot.filament:
|
if snapshot.filament:
|
||||||
filament_dict['color_name'] = snapshot.filament.color
|
filament_dict['color_name'] = snapshot.filament.color
|
||||||
filament_dict['filament_pk'] = snapshot.filament.pk
|
filament_dict['filament_pk'] = snapshot.filament.pk
|
||||||
|
filament_dict['is_transparent'] = snapshot.filament.is_transparent
|
||||||
filaments_list.append(filament_dict)
|
filaments_list.append(filament_dict)
|
||||||
except Exception:
|
except Exception:
|
||||||
filaments_list = []
|
filaments_list = []
|
||||||
@@ -544,6 +545,14 @@ class FilamentListView(LoginRequiredMixin, ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def _filament_type_map():
|
||||||
|
"""Return a JSON-serialisable dict mapping FilamentType pk → {type, sub_type, brand}."""
|
||||||
|
return {
|
||||||
|
str(ft.pk): {'type': ft.type, 'sub_type': ft.sub_type or '', 'brand': ft.brand}
|
||||||
|
for ft in FilamentType.objects.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class FilamentCreateView(LoginRequiredMixin, CreateView):
|
class FilamentCreateView(LoginRequiredMixin, CreateView):
|
||||||
model = Filament
|
model = Filament
|
||||||
form_class = FilamentForm
|
form_class = FilamentForm
|
||||||
@@ -553,6 +562,7 @@ class FilamentCreateView(LoginRequiredMixin, CreateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
||||||
|
context['filament_type_map'] = json.dumps(_filament_type_map())
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
@@ -569,6 +579,7 @@ class FilamentUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
||||||
|
context['filament_type_map'] = json.dumps(_filament_type_map())
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
Translucent #000000
|
||||||
Translucent Gray #8E8E8E
|
Translucent Gray #8E8E8E
|
||||||
Translucent Light Blue #61B0FF
|
Translucent Light Blue #61B0FF
|
||||||
Translucent Olive #748C45
|
Translucent Olive #748C45
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "bambu-run"
|
name = "bambu-run"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
description = "Django reusable app for Bambu Lab 3D printer monitoring and filament inventory management"
|
description = "Django reusable app for Bambu Lab 3D printer monitoring and filament inventory management"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
|
|||||||
Reference in New Issue
Block a user