mirror of
https://github.com/RunLit/Bambu-Run.git
synced 2026-06-22 14:09:04 +01:00
Compare commits
3 Commits
v0.1.1
...
petg_basic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e3b485eef | ||
|
|
46902d7ec0 | ||
|
|
5c56711c57 |
56
.github/workflow/bump-version.yml
vendored
Normal file
56
.github/workflow/bump-version.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Bump Patch Version on Merge to Main
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
bump-version:
|
||||
runs-on: ubuntu-latest
|
||||
# Skip if this push was itself the version bump commit (prevents infinite loop)
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Need full git history and ability to push
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Bump patch version in pyproject.toml
|
||||
id: bump
|
||||
run: |
|
||||
# Read current version
|
||||
CURRENT=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
|
||||
echo "Current version: $CURRENT"
|
||||
|
||||
# Split into parts and increment patch
|
||||
MAJOR=$(echo $CURRENT | cut -d. -f1)
|
||||
MINOR=$(echo $CURRENT | cut -d. -f2)
|
||||
PATCH=$(echo $CURRENT | cut -d. -f3)
|
||||
NEW_PATCH=$((PATCH + 1))
|
||||
NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH"
|
||||
echo "New version: $NEW_VERSION"
|
||||
|
||||
# Write back to pyproject.toml
|
||||
sed -i "s/^version = \"$CURRENT\"/version = \"$NEW_VERSION\"/" pyproject.toml
|
||||
|
||||
# Export for later steps
|
||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit and push bumped version
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add pyproject.toml
|
||||
git commit -m "chore: bump version to ${{ steps.bump.outputs.new_version }} [skip ci]"
|
||||
git push
|
||||
- name: Tag the release
|
||||
run: |
|
||||
git tag "v${{ steps.bump.outputs.new_version }}"
|
||||
git push origin "v${{ steps.bump.outputs.new_version }}"
|
||||
@@ -52,7 +52,7 @@ class FilamentForm(forms.ModelForm):
|
||||
model = Filament
|
||||
fields = [
|
||||
'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',
|
||||
'remaining_percent', 'remaining_weight_grams',
|
||||
'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'}),
|
||||
'created_by': forms.Select(attrs={'class': 'form-select'}),
|
||||
'filament_type': forms.Select(attrs={'class': 'form-select'}),
|
||||
'type': forms.HiddenInput(),
|
||||
'sub_type': forms.HiddenInput(),
|
||||
'brand': forms.HiddenInput(),
|
||||
'filament_type': forms.Select(attrs={'class': 'form-select', 'id': 'id_filament_type'}),
|
||||
'type': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., PLA, PETG, ABS'}),
|
||||
'sub_type': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., PLA Basic (optional)'}),
|
||||
'brand': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., Bambu Lab'}),
|
||||
'color': forms.Select(attrs={'class': 'form-select', 'id': 'id_color'}),
|
||||
'color_hex': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
@@ -85,6 +85,7 @@ class FilamentForm(forms.ModelForm):
|
||||
'initial_weight_grams': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': '1000'}),
|
||||
'remaining_percent': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '100'}),
|
||||
'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'}),
|
||||
'current_tray_id': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '3'}),
|
||||
'purchase_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||
|
||||
@@ -316,7 +316,7 @@ class Command(BaseCommand):
|
||||
|
||||
def _auto_create_filament(self, tray_data):
|
||||
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')
|
||||
tag_uid = tray_data.get('tag_uid')
|
||||
@@ -329,6 +329,7 @@ class Command(BaseCommand):
|
||||
|
||||
default_brand = app_settings.AUTO_CREATE_BRAND
|
||||
|
||||
transparent = is_mqtt_color_transparent(mqtt_color)
|
||||
color_code = strip_color_padding(mqtt_color)
|
||||
color_hex = f"#{color_code}" if color_code else None
|
||||
|
||||
@@ -341,6 +342,7 @@ class Command(BaseCommand):
|
||||
|
||||
if filament_color:
|
||||
color_name = filament_color.color_name
|
||||
transparent = transparent or filament_color.is_transparent
|
||||
if self.verbose:
|
||||
logger.info(f"Matched color from database: {color_name} (#{color_code})")
|
||||
else:
|
||||
@@ -368,6 +370,7 @@ class Command(BaseCommand):
|
||||
brand=default_brand,
|
||||
color=color_name,
|
||||
color_hex=color_hex,
|
||||
is_transparent=transparent,
|
||||
diameter=diameter,
|
||||
initial_weight_grams=initial_weight,
|
||||
remaining_percent=remain_percent,
|
||||
|
||||
@@ -371,6 +371,11 @@ class Command(BaseCommand):
|
||||
return "created"
|
||||
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 ──────────────────────────────────────────────────
|
||||
# All five fields must match to be considered a duplicate:
|
||||
# color_code (exact), color_name (case-insensitive), brand,
|
||||
@@ -388,9 +393,10 @@ class Command(BaseCommand):
|
||||
return "duplicate"
|
||||
|
||||
if dry_run:
|
||||
transparent_note = " [transparent]" if is_transparent else ""
|
||||
self.stdout.write(
|
||||
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"
|
||||
|
||||
@@ -404,6 +410,7 @@ class Command(BaseCommand):
|
||||
filament_type=filament_type,
|
||||
filament_sub_type=filament_sub_type,
|
||||
brand=BRAND,
|
||||
is_transparent=is_transparent,
|
||||
)
|
||||
self.stdout.write(
|
||||
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',
|
||||
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)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
@@ -329,6 +333,10 @@ class Filament(models.Model):
|
||||
max_length=7, null=True, blank=True,
|
||||
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
|
||||
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() {
|
||||
// Apply colors to filament cards
|
||||
document.querySelectorAll('.filament-card').forEach(card => {
|
||||
const isTransparent = card.getAttribute('data-filament-transparent') === 'true';
|
||||
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;
|
||||
|
||||
// Set card background with gradient
|
||||
|
||||
@@ -54,11 +54,19 @@
|
||||
{% for color in colors %}
|
||||
<tr>
|
||||
<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>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle"><strong>{{ color.color_name }}</strong></td>
|
||||
<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>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<span class="badge bg-secondary">{{ color.filament_type }}</span>
|
||||
|
||||
@@ -27,10 +27,14 @@
|
||||
<div class="card-body">
|
||||
<h6>Color</h6>
|
||||
<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>
|
||||
{% endif %}
|
||||
<div>
|
||||
<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>
|
||||
|
||||
@@ -43,6 +43,14 @@
|
||||
<hr>
|
||||
<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="col-md-3">
|
||||
<label class="form-label">Type *</label>
|
||||
@@ -62,12 +70,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="row mb-3 align-items-end">
|
||||
<div class="col-md-2">
|
||||
<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 }}
|
||||
</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>
|
||||
{{ form.color_hex_text }}
|
||||
<small class="form-text text-muted">e.g. #0A2CA5</small>
|
||||
@@ -209,95 +224,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Sync color picker and text input
|
||||
const colorPicker = document.getElementById('id_color_hex_picker');
|
||||
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>
|
||||
{# Server-side data consumed by filament_form.js #}
|
||||
<script type="application/json" id="filament-type-data">{{ filament_type_map|safe }}</script>
|
||||
<script src="{% static 'bambu_run/js/filament_form.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -122,7 +122,11 @@
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<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>
|
||||
{% endif %}
|
||||
{{ filament.color }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
<div class="row g-3">
|
||||
{% for filament in stats.filaments %}
|
||||
<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="d-flex justify-content-between align-items-center mb-2">
|
||||
<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').
|
||||
# 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
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Strip FF padding from MQTT color
|
||||
MQTT: '000000FF' -> '000000'
|
||||
Strip alpha padding from MQTT color, returning the 6-char RGB hex.
|
||||
MQTT: '000000FF' -> '000000' (opaque black)
|
||||
MQTT: '00000000' -> '000000' (transparent — use is_mqtt_color_transparent() to distinguish)
|
||||
MQTT: 'FF6A13FF' -> 'FF6A13'
|
||||
"""
|
||||
if not mqtt_color:
|
||||
|
||||
@@ -125,6 +125,7 @@ class PrinterDashboardView(LoginRequiredMixin, TemplateView):
|
||||
if snapshot.filament:
|
||||
filament_dict['color_name'] = snapshot.filament.color
|
||||
filament_dict['filament_pk'] = snapshot.filament.pk
|
||||
filament_dict['is_transparent'] = snapshot.filament.is_transparent
|
||||
filaments_list.append(filament_dict)
|
||||
except Exception:
|
||||
filaments_list = []
|
||||
@@ -544,6 +545,14 @@ class FilamentListView(LoginRequiredMixin, ListView):
|
||||
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):
|
||||
model = Filament
|
||||
form_class = FilamentForm
|
||||
@@ -553,6 +562,7 @@ class FilamentCreateView(LoginRequiredMixin, CreateView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
||||
context['filament_type_map'] = json.dumps(_filament_type_map())
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
@@ -569,6 +579,7 @@ class FilamentUpdateView(LoginRequiredMixin, UpdateView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
||||
context['filament_type_map'] = json.dumps(_filament_type_map())
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
|
||||
7
docs/Bambu_Color_Catalog/PETG Basic.txt
Normal file
7
docs/Bambu_Color_Catalog/PETG Basic.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Red #D6001C
|
||||
Yellow #FCE300
|
||||
Reflex Blue #001489
|
||||
Black #000000
|
||||
Gray #7F7E83
|
||||
Dark Brown #4F2C1D
|
||||
White #FFFFFF
|
||||
@@ -1,3 +1,4 @@
|
||||
Translucent #000000
|
||||
Translucent Gray #8E8E8E
|
||||
Translucent Light Blue #61B0FF
|
||||
Translucent Olive #748C45
|
||||
|
||||
4
docs/Bambu_Color_Catalog/PLA Galaxy.txt
Normal file
4
docs/Bambu_Color_Catalog/PLA Galaxy.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
Brown #684A43
|
||||
Green #3B665E
|
||||
Nebulae #424379
|
||||
Purple #594177
|
||||
2
docs/Bambu_Color_Catalog/PLA Marble.txt
Normal file
2
docs/Bambu_Color_Catalog/PLA Marble.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
White Marble #F7F3F0
|
||||
Red Granite #AD4E38
|
||||
5
docs/Bambu_Color_Catalog/PLA Metal.txt
Normal file
5
docs/Bambu_Color_Catalog/PLA Metal.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Iridium Gold Metallic #B39B84
|
||||
Copper Brown Metallic #AA6443
|
||||
Oxide Green Metallic #1D7C6A
|
||||
Cobalt Blue Metallic #39699E
|
||||
Iron Gray Metallic #43403D
|
||||
13
docs/Bambu_Color_Catalog/PLA Silk.txt
Normal file
13
docs/Bambu_Color_Catalog/PLA Silk.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
Gold #F4A925
|
||||
Silver #C8C8C8
|
||||
Titan Gray #5F6367
|
||||
Blue #008BDA
|
||||
Purple #8671CB
|
||||
Candy Red #D02727
|
||||
Candy Green #018814
|
||||
Rose Gold #BA9594
|
||||
Baby Blue #A8C6EE
|
||||
Pink #F7ADA6
|
||||
Mint #96DCB9
|
||||
Champagne #F3CFB2
|
||||
White #FFFFFF
|
||||
5
docs/Bambu_Color_Catalog/PLA Sparkle.txt
Normal file
5
docs/Bambu_Color_Catalog/PLA Sparkle.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Slate Gray Sparkle #8E9089
|
||||
Crimson Red Sparkle #792B36
|
||||
Royal Purple Sparkle #483D8B
|
||||
Alpine Green Sparkle #3F5443
|
||||
Onyx Black Sparkle #2D2B28
|
||||
10
docs/Bambu_Color_Catalog/PLA Translucent.txt
Normal file
10
docs/Bambu_Color_Catalog/PLA Translucent.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Teal Hex:#009FA1
|
||||
Light Jade Hex:#96D8AF
|
||||
Blue Hex:#0047BB
|
||||
Mellow Yellow Hex:#F5DBAB
|
||||
Purple Hex:#8344B0
|
||||
Cherry Pink Hex:#F5B6CD
|
||||
Orange Hex:#F74E02
|
||||
Ice Blue Hex:#B8CDE9
|
||||
Red Hex:#B50011
|
||||
Lavender Hex:#B8ACD6
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
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"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
||||
Reference in New Issue
Block a user