init
This commit is contained in:
70
html/bambu_filaments.json
Normal file
70
html/bambu_filaments.json
Normal file
@ -0,0 +1,70 @@
|
||||
{
|
||||
"GFU99": "Generic TPU",
|
||||
"GFN99": "Generic PA",
|
||||
"GFN98": "Generic PA-CF",
|
||||
"GFA01": "Bambu PLA Matte",
|
||||
"GFA00": "Bambu PLA Basic",
|
||||
"GFA09": "Bambu PLA Tough",
|
||||
"GFA07": "Bambu PLA Marble",
|
||||
"GFA08": "Bambu PLA Sparkle",
|
||||
"GFA02": "Bambu PLA Metal",
|
||||
"GFA05": "Bambu PLA Silk",
|
||||
"GFS00": "Bambu Support W",
|
||||
"GFL03": "eSUN PLA+",
|
||||
"GFL01": "PolyTerra PLA",
|
||||
"GFL00": "PolyLite PLA",
|
||||
"GFL99": "Generic PLA",
|
||||
"GFL96": "Generic PLA Silk",
|
||||
"GFL98": "Generic PLA-CF",
|
||||
"GFA50": "Bambu PLA-CF",
|
||||
"GFS02": "Bambu Support For PLA",
|
||||
"GFA11": "Bambu PLA Aero",
|
||||
"GFL04": "Overture PLA",
|
||||
"GFL05": "Overture Matte PLA",
|
||||
"GFL95": "Generic PLA High Speed",
|
||||
"GFA12": "Bambu PLA Glow",
|
||||
"GFA13": "Bambu PLA Dynamic",
|
||||
"GFA15": "Bambu PLA Galaxy",
|
||||
"GFS05": "Bambu Support For PLA/PETG",
|
||||
"GFU01": "Bambu TPU 95A",
|
||||
"GFU00": "Bambu TPU 95A HF",
|
||||
"GFG00": "Bambu PETG Basic",
|
||||
"GFT01": "Bambu PET-CF",
|
||||
"GFG99": "Generic PETG",
|
||||
"GFG98": "Generic PETG-CF",
|
||||
"GFG50": "Bambu PETG-CF",
|
||||
"GFG60": "PolyLite PETG",
|
||||
"GFG01": "Bambu PETG Translucent",
|
||||
"GFG97": "Generic PCTG",
|
||||
"GFB00": "Bambu ABS",
|
||||
"GFB99": "Generic ABS",
|
||||
"GFB60": "PolyLite ABS",
|
||||
"GFB50": "Bambu ABS-GF",
|
||||
"GFC00": "Bambu PC",
|
||||
"GFC99": "Generic PC",
|
||||
"GFB98": "Generic ASA",
|
||||
"GFB01": "Bambu ASA",
|
||||
"GFB61": "PolyLite ASA",
|
||||
"GFB02": "Bambu ASA-Aero",
|
||||
"GFS99": "Generic PVA",
|
||||
"GFS04": "Bambu PVA",
|
||||
"GFS01": "Bambu Support G",
|
||||
"GFN03": "Bambu PA-CF",
|
||||
"GFN04": "Bambu PAHT-CF",
|
||||
"GFS03": "Bambu Support For PA/PET",
|
||||
"GFN05": "Bambu PA6-CF",
|
||||
"GFN08": "Bambu PA6-GF",
|
||||
"GFS98": "Generic HIPS",
|
||||
"GFT98": "Generic PPS-CF",
|
||||
"GFT97": "Generic PPS",
|
||||
"GFN97": "Generic PPA-CF",
|
||||
"GFN96": "Generic PPA-GF",
|
||||
"GFP99": "Generic PE",
|
||||
"GFP98": "Generic PE-CF",
|
||||
"GFP97": "Generic PP",
|
||||
"GFP96": "Generic PP-CF",
|
||||
"GFP95": "Generic PP-GF",
|
||||
"GFR99": "Generic EVA",
|
||||
"GFR98": "Generic PHA",
|
||||
"GFS97": "Generic BVOH"
|
||||
}
|
BIN
html/favicon.ico
Normal file
BIN
html/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
51
html/header.html
Normal file
51
html/header.html
Normal file
@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FilaMan - Filament Management Tool</title>
|
||||
<link rel="icon" type="image/png" href="/favicon.ico">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>
|
||||
.status-container {
|
||||
float: right;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.8em;
|
||||
color: #fff;
|
||||
}
|
||||
.online { background-color: #2ecc71; }
|
||||
.offline { background-color: #e74c3c; }
|
||||
.ram-status { color: #fff; font-size: 0.8em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar">
|
||||
<img src="/logo.png" alt="FilaMan Logo" class="logo">
|
||||
<a href="/">Start</a>
|
||||
<a href="/waage">Scale</a>
|
||||
<a href="/spoolman">Spoolman/Bambu</a>
|
||||
<a href="/about">About</a>
|
||||
<div class="status-container">
|
||||
<div class="status-item">
|
||||
<span class="status-dot" id="bambuDot"></span>B
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-dot" id="spoolmanDot"></span>S
|
||||
</div>
|
||||
<div class="ram-status" id="ramStatus"></div>
|
||||
</div>
|
||||
</div>
|
37
html/index.html
Normal file
37
html/index.html
Normal file
@ -0,0 +1,37 @@
|
||||
{{header}}
|
||||
|
||||
<div class="container">
|
||||
<h1>FilaMan</h1>
|
||||
<p>Filament Management Tool</p>
|
||||
<p>Your smart solution for <strong>Filament Management</strong> in 3D printing.</p>
|
||||
|
||||
<h2>About FilaMan</h2>
|
||||
<p>
|
||||
FilaMan is a tool designed to simplify filament spool management. It allows you to identify and weigh filament spools,
|
||||
automatically sync data with the self-hosted <a href="https://github.com/Donkie/Spoolman" target="_blank">Spoolman</a> platform,
|
||||
and ensure compatibility with <a href="https://github.com/spuder/OpenSpool" target="_blank">OpenSpool</a> for Bambu printers.
|
||||
</p>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature">
|
||||
<h3>Spool Identification</h3>
|
||||
<p>Easily identify filament spools using NFC tags (NTag215 or larger).</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>Automatic Syncing</h3>
|
||||
<p>Seamlessly update spool data with Spoolman for accurate tracking.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>OpenSpool Compatibility</h3>
|
||||
<p>Works with OpenSpool to recognize and activate spools on Bambu printers.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Future Plans</h2>
|
||||
<p>
|
||||
We are working on expanding compatibility to support smaller NFC tags like NTag213
|
||||
and developing custom software to enhance the OpenSpool experience.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
BIN
html/logo.png
Normal file
BIN
html/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
114
html/rfid.html
Normal file
114
html/rfid.html
Normal file
@ -0,0 +1,114 @@
|
||||
{{header}}
|
||||
<div class="connection-status hidden">
|
||||
<div class="spinner"></div>
|
||||
<span>Connection lost. Trying to reconnect...</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="three-column-layout">
|
||||
<!-- Linke Spalte -->
|
||||
<div class="column">
|
||||
<div class="feature-box">
|
||||
<div class="statistics-header">
|
||||
<h2>Statistics</h2>
|
||||
<button id="refreshSpoolman" class="refresh-button">
|
||||
<span>Refresh Spoolman</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="statistics-column">
|
||||
<h3>Spools</h3>
|
||||
<div class="spool-stat" style="display: flex; justify-content: center; align-items: center;">
|
||||
<span class="stat-label">total:</span>
|
||||
<span class="stat-value" id="totalSpools"></span>
|
||||
<div style="width: auto;"></div>
|
||||
<span class="stat-label">without Tag:</span>
|
||||
<span class="stat-value" id="spoolsWithoutTag"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="statistics-grid">
|
||||
<div class="statistics-column">
|
||||
<h3>Overview</h3>
|
||||
<ul class="statistics-list">
|
||||
<li>
|
||||
<span class="stat-label">Manufacturer:</span>
|
||||
<span class="stat-value" id="totalVendors"></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="stat-label">Weight:</span>
|
||||
<span class="stat-value"><span id="totalWeight"></span> kg</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="stat-label">Length:</span>
|
||||
<span class="stat-value"><span id="totalLength"></span> m</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="statistics-column">
|
||||
<h3>Materials</h3>
|
||||
<ul class="statistics-list" id="materialsList">
|
||||
<!-- Wird dynamisch befüllt -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-box">
|
||||
<div class="nfc-header">
|
||||
<h2>NFC-Tag</h2>
|
||||
<span id="nfcStatusIndicator" class="status-circle"></span>
|
||||
</div>
|
||||
<div class="nfc-status-display"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mittlere Spalte -->
|
||||
<div class="column">
|
||||
<div class="feature-box">
|
||||
<h2>Spoolman Spools</h2>
|
||||
<h2>1. select Manufacturer</h2>
|
||||
<label for="vendorSelect">Manufacturer:</label>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<select id="vendorSelect" class="styled-select">
|
||||
<option value="">Please choose...</option>
|
||||
</select>
|
||||
<label style="margin-left: 10px;">
|
||||
<input type="checkbox" id="onlyWithoutSmId" checked onchange="updateFilamentDropdown()">
|
||||
Only Spools without SM ID
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="filamentSection" class="feature-box hidden">
|
||||
<h2>2. Select Spool</h2>
|
||||
<label>Spool / Filament:</label>
|
||||
<div class="custom-dropdown">
|
||||
<div class="dropdown-button" onclick="toggleFilamentDropdown()">
|
||||
<div class="selected-color" id="selected-color"></div>
|
||||
<span id="selected-filament">Please choose...</span>
|
||||
<span class="dropdown-arrow">▼</span>
|
||||
</div>
|
||||
<div class="dropdown-content" id="filament-dropdown-content">
|
||||
<!-- Optionen werden dynamisch hinzugefügt -->
|
||||
</div>
|
||||
</div>
|
||||
<p id="nfcInfo" class="nfc-status"></p>
|
||||
<button id="writeNfcButton" class="btn btn-primary hidden" onclick="writeNfcTag()">Write Tag</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rechte Spalte -->
|
||||
<div class="column">
|
||||
<div class="feature-box">
|
||||
<h2>Bambu AMS</h2>
|
||||
<div id="amsDataContainer">
|
||||
<div class="amsData" id="amsData">Wait for AMS-Data...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="spoolman.js"></script>
|
||||
<script src="rfid.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
570
html/rfid.js
Normal file
570
html/rfid.js
Normal file
@ -0,0 +1,570 @@
|
||||
// WebSocket Variablen
|
||||
let socket;
|
||||
let isConnected = false;
|
||||
const RECONNECT_INTERVAL = 5000;
|
||||
const HEARTBEAT_INTERVAL = 10000;
|
||||
let heartbeatTimer = null;
|
||||
let lastHeartbeatResponse = Date.now();
|
||||
const HEARTBEAT_TIMEOUT = 20000;
|
||||
let reconnectTimer = null;
|
||||
|
||||
// WebSocket Funktionen
|
||||
function startHeartbeat() {
|
||||
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
||||
|
||||
heartbeatTimer = setInterval(() => {
|
||||
// Prüfe ob zu lange keine Antwort kam
|
||||
if (Date.now() - lastHeartbeatResponse > HEARTBEAT_TIMEOUT) {
|
||||
isConnected = false;
|
||||
updateConnectionStatus();
|
||||
if (socket) {
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
isConnected = false;
|
||||
updateConnectionStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Sende Heartbeat
|
||||
socket.send(JSON.stringify({ type: 'heartbeat' }));
|
||||
} catch (error) {
|
||||
isConnected = false;
|
||||
updateConnectionStatus();
|
||||
if (socket) {
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL);
|
||||
}
|
||||
|
||||
function initWebSocket() {
|
||||
// Clear any existing reconnect timer
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
|
||||
// Wenn eine existierende Verbindung besteht, diese erst schließen
|
||||
if (socket) {
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
|
||||
try {
|
||||
socket = new WebSocket('ws://' + window.location.host + '/ws');
|
||||
|
||||
socket.onopen = function() {
|
||||
isConnected = true;
|
||||
updateConnectionStatus();
|
||||
startHeartbeat(); // Starte Heartbeat nach erfolgreicher Verbindung
|
||||
};
|
||||
|
||||
socket.onclose = function() {
|
||||
isConnected = false;
|
||||
updateConnectionStatus();
|
||||
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
||||
|
||||
// Nur neue Verbindung versuchen, wenn kein Timer läuft
|
||||
if (!reconnectTimer) {
|
||||
reconnectTimer = setTimeout(() => {
|
||||
initWebSocket();
|
||||
}, RECONNECT_INTERVAL);
|
||||
}
|
||||
};
|
||||
|
||||
socket.onerror = function(error) {
|
||||
isConnected = false;
|
||||
updateConnectionStatus();
|
||||
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
||||
|
||||
// Bei Fehler Verbindung schließen und neu aufbauen
|
||||
if (socket) {
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
};
|
||||
|
||||
socket.onmessage = function(event) {
|
||||
lastHeartbeatResponse = Date.now(); // Aktualisiere Zeitstempel bei jeder Server-Antwort
|
||||
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'amsData') {
|
||||
displayAmsData(data.payload);
|
||||
} else if (data.type === 'nfcTag') {
|
||||
updateNfcStatusIndicator(data.payload);
|
||||
} else if (data.type === 'nfcData') {
|
||||
updateNfcData(data.payload);
|
||||
} else if (data.type === 'writeNfcTag') {
|
||||
handleWriteNfcTagResponse(data.success);
|
||||
} else if (data.type === 'heartbeat') {
|
||||
// Optional: Spezifische Behandlung von Heartbeat-Antworten
|
||||
// Update status dots
|
||||
const bambuDot = document.getElementById('bambuDot');
|
||||
const spoolmanDot = document.getElementById('spoolmanDot');
|
||||
const ramStatus = document.getElementById('ramStatus');
|
||||
|
||||
if (bambuDot) {
|
||||
bambuDot.className = 'status-dot ' + (data.bambu_connected ? 'online' : 'offline');
|
||||
}
|
||||
if (spoolmanDot) {
|
||||
spoolmanDot.className = 'status-dot ' + (data.spoolman_connected ? 'online' : 'offline');
|
||||
}
|
||||
if (ramStatus) {
|
||||
ramStatus.textContent = `${data.freeHeap}k`;
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
isConnected = false;
|
||||
updateConnectionStatus();
|
||||
|
||||
// Nur neue Verbindung versuchen, wenn kein Timer läuft
|
||||
if (!reconnectTimer) {
|
||||
reconnectTimer = setTimeout(() => {
|
||||
initWebSocket();
|
||||
}, RECONNECT_INTERVAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateConnectionStatus() {
|
||||
const statusElement = document.querySelector('.connection-status');
|
||||
if (!isConnected) {
|
||||
statusElement.classList.remove('hidden');
|
||||
// Verzögerung hinzufügen, damit die CSS-Transition wirken kann
|
||||
setTimeout(() => {
|
||||
statusElement.classList.add('visible');
|
||||
}, 10);
|
||||
} else {
|
||||
statusElement.classList.remove('visible');
|
||||
// Warte auf das Ende der Fade-out Animation bevor wir hidden setzen
|
||||
setTimeout(() => {
|
||||
statusElement.classList.add('hidden');
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Event Listeners
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
initWebSocket();
|
||||
|
||||
// Event Listener für Checkbox
|
||||
document.getElementById("onlyWithoutSmId").addEventListener("change", function() {
|
||||
const spoolsData = window.getSpoolData();
|
||||
window.populateVendorDropdown(spoolsData);
|
||||
});
|
||||
});
|
||||
|
||||
// Event Listener für Spoolman Events
|
||||
document.addEventListener('spoolDataLoaded', function(event) {
|
||||
window.populateVendorDropdown(event.detail);
|
||||
});
|
||||
|
||||
document.addEventListener('spoolmanError', function(event) {
|
||||
showNotification(`Spoolman Error: ${event.detail.message}`, false);
|
||||
});
|
||||
|
||||
document.addEventListener('filamentSelected', function(event) {
|
||||
updateNfcInfo();
|
||||
// Zeige Spool-Buttons wenn ein Filament ausgewählt wurde
|
||||
const selectedText = document.getElementById("selected-filament").textContent;
|
||||
updateSpoolButtons(selectedText !== "Please choose...");
|
||||
});
|
||||
|
||||
// Hilfsfunktion für kontrastreiche Textfarbe
|
||||
function getContrastColor(hexcolor) {
|
||||
// Konvertiere Hex zu RGB
|
||||
const r = parseInt(hexcolor.substr(0,2),16);
|
||||
const g = parseInt(hexcolor.substr(2,2),16);
|
||||
const b = parseInt(hexcolor.substr(4,2),16);
|
||||
|
||||
// Berechne Helligkeit (YIQ Formel)
|
||||
const yiq = ((r*299)+(g*587)+(b*114))/1000;
|
||||
|
||||
// Return schwarz oder weiß basierend auf Helligkeit
|
||||
return (yiq >= 128) ? '#000000' : '#FFFFFF';
|
||||
}
|
||||
|
||||
function updateNfcInfo() {
|
||||
const selectedText = document.getElementById("selected-filament").textContent;
|
||||
const nfcInfo = document.getElementById("nfcInfo");
|
||||
const writeButton = document.getElementById("writeNfcButton");
|
||||
|
||||
if (selectedText === "Please choose...") {
|
||||
nfcInfo.textContent = "No Filament selected";
|
||||
nfcInfo.classList.remove("nfc-success", "nfc-error");
|
||||
writeButton.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
// Finde die ausgewählte Spule in den Daten
|
||||
const selectedSpool = spoolsData.find(spool =>
|
||||
`${spool.id} | ${spool.filament.name} (${spool.filament.material})` === selectedText
|
||||
);
|
||||
|
||||
if (selectedSpool && selectedSpool.extra.nfc_id) {
|
||||
nfcInfo.textContent = "NFC Tag assigned";
|
||||
nfcInfo.classList.add("nfc-success");
|
||||
nfcInfo.classList.remove("nfc-error");
|
||||
} else {
|
||||
nfcInfo.textContent = "No NFC-Tag assigned";
|
||||
nfcInfo.classList.add("nfc-error");
|
||||
nfcInfo.classList.remove("nfc-success");
|
||||
}
|
||||
|
||||
if (selectedSpool) {
|
||||
writeButton.classList.remove("hidden");
|
||||
} else {
|
||||
writeButton.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function displayAmsData(amsData) {
|
||||
const amsDataContainer = document.getElementById('amsData');
|
||||
amsDataContainer.innerHTML = '';
|
||||
|
||||
amsData.forEach((ams) => {
|
||||
// Bestimme den Anzeigenamen für das AMS
|
||||
const amsDisplayName = ams.ams_id === 255 ? 'External Spool' : `AMS ${ams.ams_id}`;
|
||||
|
||||
const trayHTML = ams.tray.map(tray => {
|
||||
// Prüfe ob überhaupt Daten vorhanden sind
|
||||
const relevantFields = ['tray_type', 'tray_sub_brands', 'tray_info_idx', 'setting_id'];
|
||||
const hasAnyContent = relevantFields.some(field =>
|
||||
tray[field] !== null &&
|
||||
tray[field] !== undefined &&
|
||||
tray[field] !== '' &&
|
||||
tray[field] !== 'null'
|
||||
);
|
||||
|
||||
if (!hasAnyContent) {
|
||||
return `
|
||||
<div class="tray">
|
||||
<p><b>Tray ${tray.id}</b></p>
|
||||
<p>Empty</p>
|
||||
</div>
|
||||
<hr>`;
|
||||
}
|
||||
|
||||
// Nur für nicht-leere Trays den Button-HTML erstellen
|
||||
const buttonHtml = `
|
||||
<button class="spool-button" onclick="handleSpoolIn(${ams.ams_id}, ${tray.id})"
|
||||
style="position: absolute; top: 5px; left: 5px;
|
||||
background: none; border: none; padding: 0;
|
||||
cursor: pointer; display: none;">
|
||||
<img src="spool_in.png" alt="Spool In" style="width: 48px; height: 48px;">
|
||||
</button>`;
|
||||
|
||||
// Generiere den Type mit Color-Box zusammen
|
||||
const typeWithColor = tray.tray_type ?
|
||||
`<p>Typ: ${tray.tray_type} ${tray.tray_color ? `<span style="
|
||||
background-color: #${tray.tray_color};
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
border: 1px solid #333;
|
||||
border-radius: 3px;
|
||||
margin-left: 5px;"></span>` : ''}</p>` : '';
|
||||
|
||||
// Array mit restlichen Tray-Eigenschaften
|
||||
const trayProperties = [
|
||||
{ key: 'tray_sub_brands', label: 'Sub Brands' },
|
||||
{ key: 'tray_info_idx', label: 'Filament Index' },
|
||||
{ key: 'setting_id', label: 'Setting ID' }
|
||||
];
|
||||
|
||||
// Nur gültige Felder anzeigen
|
||||
const trayDetails = trayProperties
|
||||
.filter(prop =>
|
||||
tray[prop.key] !== null &&
|
||||
tray[prop.key] !== undefined &&
|
||||
tray[prop.key] !== '' &&
|
||||
tray[prop.key] !== 'null'
|
||||
)
|
||||
.map(prop => `<p>${prop.label}: ${tray[prop.key]}</p>`)
|
||||
.join('');
|
||||
|
||||
// Temperaturen nur anzeigen, wenn beide nicht 0 sind
|
||||
const tempHTML = (tray.nozzle_temp_min > 0 && tray.nozzle_temp_max > 0)
|
||||
? `<p>Nozzle Temp: ${tray.nozzle_temp_min}°C - ${tray.nozzle_temp_max}°C</p>`
|
||||
: '';
|
||||
|
||||
// Bestimme den Anzeigenamen für das Tray
|
||||
const trayDisplayName = (ams.ams_id === 255) ? 'External' : `Tray ${tray.id}`;
|
||||
|
||||
return `
|
||||
<div class="tray" ${tray.tray_color ? `style="border-left: 4px solid #${tray.tray_color};"` : 'style="border-left: 4px solid #007bff;"'}>
|
||||
<div style="position: relative;">
|
||||
${buttonHtml}
|
||||
<p><b>${trayDisplayName}</b></p>
|
||||
${typeWithColor}
|
||||
${trayDetails}
|
||||
${tempHTML}
|
||||
</div>
|
||||
</div>
|
||||
<hr>`;
|
||||
}).join('');
|
||||
|
||||
const amsInfo = `
|
||||
<div class="feature">
|
||||
<h3>${amsDisplayName}:</h3>
|
||||
<div id="trayContainer">
|
||||
${trayHTML}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
amsDataContainer.innerHTML += amsInfo;
|
||||
});
|
||||
}
|
||||
|
||||
// Neue Funktion zum Anzeigen/Ausblenden der Spool-Buttons
|
||||
function updateSpoolButtons(show) {
|
||||
const spoolButtons = document.querySelectorAll('.spool-button');
|
||||
spoolButtons.forEach(button => {
|
||||
button.style.display = show ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Neue Funktion zum Behandeln des Spool-In-Klicks
|
||||
function handleSpoolIn(amsId, trayId) {
|
||||
// Prüfe WebSocket Verbindung zuerst
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
showNotification("No active WebSocket connection!", false);
|
||||
console.error("WebSocket not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
// Hole das ausgewählte Filament
|
||||
const selectedText = document.getElementById("selected-filament").textContent;
|
||||
if (selectedText === "Please choose...") {
|
||||
showNotification("Choose Filament first", false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Finde die ausgewählte Spule in den Daten
|
||||
const selectedSpool = spoolsData.find(spool =>
|
||||
`${spool.id} | ${spool.filament.name} (${spool.filament.material})` === selectedText
|
||||
);
|
||||
|
||||
if (!selectedSpool) {
|
||||
showNotification("Selected Spool not found", false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Temperaturwerte extrahieren
|
||||
let minTemp = "175";
|
||||
let maxTemp = "275";
|
||||
|
||||
if (Array.isArray(selectedSpool.filament.nozzle_temperature) &&
|
||||
selectedSpool.filament.nozzle_temperature.length >= 2) {
|
||||
minTemp = selectedSpool.filament.nozzle_temperature[0];
|
||||
maxTemp = selectedSpool.filament.nozzle_temperature[1];
|
||||
}
|
||||
|
||||
// Erstelle Payload
|
||||
const payload = {
|
||||
type: 'setBambuSpool',
|
||||
payload: {
|
||||
amsId: amsId,
|
||||
trayId: trayId,
|
||||
color: selectedSpool.filament.color_hex || "FFFFFF",
|
||||
nozzle_temp_min: parseInt(minTemp),
|
||||
nozzle_temp_max: parseInt(maxTemp),
|
||||
type: selectedSpool.filament.material,
|
||||
brand: selectedSpool.filament.vendor.name
|
||||
}
|
||||
};
|
||||
|
||||
// Debug logging
|
||||
console.log("Sende WebSocket Nachricht:", payload);
|
||||
|
||||
try {
|
||||
socket.send(JSON.stringify(payload));
|
||||
showNotification(`Spool set in AMS ${amsId} Tray ${trayId}. Pls wait`, true);
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Senden der WebSocket Nachricht:", error);
|
||||
showNotification("Fehler beim Senden der Daten", false);
|
||||
}
|
||||
}
|
||||
|
||||
function updateNfcStatusIndicator(data) {
|
||||
const indicator = document.getElementById('nfcStatusIndicator');
|
||||
|
||||
if (data.found === 0) {
|
||||
// Kein NFC Tag gefunden
|
||||
indicator.className = 'status-circle';
|
||||
} else if (data.found === 1) {
|
||||
// NFC Tag erfolgreich gelesen
|
||||
indicator.className = 'status-circle success';
|
||||
} else {
|
||||
// Fehler beim Lesen
|
||||
indicator.className = 'status-circle error';
|
||||
}
|
||||
}
|
||||
|
||||
function updateNfcData(data) {
|
||||
// Den Container für den NFC Status finden
|
||||
const nfcStatusContainer = document.querySelector('.nfc-status-display');
|
||||
|
||||
// Bestehende Daten-Anzeige entfernen falls vorhanden
|
||||
const existingData = nfcStatusContainer.querySelector('.nfc-data');
|
||||
if (existingData) {
|
||||
existingData.remove();
|
||||
}
|
||||
|
||||
// Neues div für die Datenanzeige erstellen
|
||||
const nfcDataDiv = document.createElement('div');
|
||||
nfcDataDiv.className = 'nfc-data';
|
||||
|
||||
// Wenn ein Fehler vorliegt oder keine Daten vorhanden sind
|
||||
if (data.error || data.info || !data || Object.keys(data).length === 0) {
|
||||
// Zeige Fehlermeldung oder leere Nachricht
|
||||
if (data.error || data.info) {
|
||||
if (data.error) {
|
||||
nfcDataDiv.innerHTML = `
|
||||
<div class="error-message" style="margin-top: 10px; color: #dc3545;">
|
||||
<p><strong>Error:</strong> ${data.error}</p>
|
||||
</div>`;
|
||||
} else {
|
||||
nfcDataDiv.innerHTML = `
|
||||
<div class="info-message" style="margin-top: 10px; color:rgb(18, 210, 0);">
|
||||
<p><strong>Info:</strong> ${data.info}</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
} else {
|
||||
nfcDataDiv.innerHTML = '<div style="margin-top: 10px;"></div>';
|
||||
}
|
||||
nfcStatusContainer.appendChild(nfcDataDiv);
|
||||
return;
|
||||
}
|
||||
|
||||
// HTML für die Datenanzeige erstellen
|
||||
let html = `
|
||||
<div style="margin-top: 10px;">
|
||||
<p><strong>Brand:</strong> ${data.brand || 'N/A'}</p>
|
||||
<p><strong>Type:</strong> ${data.type || 'N/A'} ${data.color_hex ? `<span style="
|
||||
background-color: #${data.color_hex};
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
border: 1px solid #333;
|
||||
border-radius: 3px;
|
||||
margin-left: 5px;
|
||||
"></span>` : ''}</p>
|
||||
`;
|
||||
|
||||
// Spoolman ID anzeigen
|
||||
html += `<p><strong>Spoolman ID:</strong> ${data.sm_id || 'No Spoolman ID'}</p>`;
|
||||
|
||||
// Nur wenn eine sm_id vorhanden ist, aktualisiere die Dropdowns
|
||||
if (data.sm_id) {
|
||||
const matchingSpool = spoolsData.find(spool => spool.id === parseInt(data.sm_id));
|
||||
if (matchingSpool) {
|
||||
// Zuerst Hersteller-Dropdown aktualisieren
|
||||
document.getElementById("vendorSelect").value = matchingSpool.filament.vendor.id;
|
||||
|
||||
// Dann Filament-Dropdown aktualisieren und Spule auswählen
|
||||
updateFilamentDropdown();
|
||||
setTimeout(() => {
|
||||
// Warte kurz bis das Dropdown aktualisiert wurde
|
||||
selectFilament(matchingSpool);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
nfcDataDiv.innerHTML = html;
|
||||
|
||||
|
||||
// Neues div zum Container hinzufügen
|
||||
nfcStatusContainer.appendChild(nfcDataDiv);
|
||||
}
|
||||
|
||||
function writeNfcTag() {
|
||||
const selectedText = document.getElementById("selected-filament").textContent;
|
||||
if (selectedText === "Please choose...") {
|
||||
alert('Please select a Spool first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const spoolsData = window.getSpoolData();
|
||||
const selectedSpool = spoolsData.find(spool =>
|
||||
`${spool.id} | ${spool.filament.name} (${spool.filament.material})` === selectedText
|
||||
);
|
||||
|
||||
if (!selectedSpool) {
|
||||
alert('Ausgewählte Spule konnte nicht gefunden werden.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Temperaturwerte korrekt extrahieren
|
||||
let minTemp = "175";
|
||||
let maxTemp = "275";
|
||||
|
||||
if (Array.isArray(selectedSpool.filament.nozzle_temperature) &&
|
||||
selectedSpool.filament.nozzle_temperature.length >= 2) {
|
||||
minTemp = String(selectedSpool.filament.nozzle_temperature[0]);
|
||||
maxTemp = String(selectedSpool.filament.nozzle_temperature[1]);
|
||||
}
|
||||
|
||||
// Erstelle das NFC-Datenpaket mit korrekten Datentypen
|
||||
const nfcData = {
|
||||
version: "2.0",
|
||||
protocol: "openspool",
|
||||
color_hex: selectedSpool.filament.color_hex || "FFFFFF",
|
||||
type: selectedSpool.filament.material,
|
||||
min_temp: minTemp,
|
||||
max_temp: maxTemp,
|
||||
brand: selectedSpool.filament.vendor.name,
|
||||
sm_id: String(selectedSpool.id) // Konvertiere zu String
|
||||
};
|
||||
|
||||
if (socket?.readyState === WebSocket.OPEN) {
|
||||
const writeButton = document.getElementById("writeNfcButton");
|
||||
writeButton.classList.add("writing");
|
||||
writeButton.textContent = "Writing";
|
||||
socket.send(JSON.stringify({
|
||||
type: 'writeNfcTag',
|
||||
payload: nfcData
|
||||
}));
|
||||
} else {
|
||||
alert('Not connected to Server. Please check connection.');
|
||||
}
|
||||
}
|
||||
|
||||
function handleWriteNfcTagResponse(success) {
|
||||
const writeButton = document.getElementById("writeNfcButton");
|
||||
writeButton.classList.remove("writing");
|
||||
writeButton.classList.add(success ? "success" : "error");
|
||||
writeButton.textContent = success ? "Write success" : "Write failed";
|
||||
|
||||
setTimeout(() => {
|
||||
writeButton.classList.remove("success", "error");
|
||||
writeButton.textContent = "Write Tag";
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function showNotification(message, isSuccess) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification ${isSuccess ? 'success' : 'error'}`;
|
||||
notification.textContent = message;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Nach 3 Sekunden ausblenden
|
||||
setTimeout(() => {
|
||||
notification.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
BIN
html/spool_in.png
Normal file
BIN
html/spool_in.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
80
html/spoolman.html
Normal file
80
html/spoolman.html
Normal file
@ -0,0 +1,80 @@
|
||||
{{header}}
|
||||
<script>
|
||||
window.onload = function() {
|
||||
if (spoolmanUrl && spoolmanUrl.trim() !== "") {
|
||||
document.getElementById('spoolmanUrl').value = spoolmanUrl;
|
||||
}
|
||||
};
|
||||
|
||||
function checkSpoolmanInstance() {
|
||||
const url = document.getElementById('spoolmanUrl').value;
|
||||
fetch(`/api/checkSpoolman?url=${encodeURIComponent(url)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.healthy) {
|
||||
document.getElementById('statusMessage').innerText = 'Spoolman-Instance is availabe and healthy!';
|
||||
} else {
|
||||
document.getElementById('statusMessage').innerText = 'Spoolman-Instance not available.';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('statusMessage').innerText = 'Error while connecting to Spoolman-Instance: ' + error.message;
|
||||
});
|
||||
}
|
||||
|
||||
function saveBambuCredentials() {
|
||||
const ip = document.getElementById('bambuIp').value;
|
||||
const serial = document.getElementById('bambuSerial').value;
|
||||
const code = document.getElementById('bambuCode').value;
|
||||
|
||||
fetch(`/api/bambu?bambu_ip=${encodeURIComponent(ip)}&bambu_serialnr=${encodeURIComponent(serial)}&bambu_accesscode=${encodeURIComponent(code)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.healthy) {
|
||||
document.getElementById('bambuStatusMessage').innerText = 'Bambu Credentials saved!';
|
||||
// Erstelle und zeige den Reboot-Button
|
||||
const rebootBtn = document.createElement('button');
|
||||
rebootBtn.innerText = 'Reboot now';
|
||||
rebootBtn.className = 'reboot-button';
|
||||
rebootBtn.onclick = () => window.location.href = '/reboot';
|
||||
document.getElementById('bambuStatusMessage').appendChild(rebootBtn);
|
||||
} else {
|
||||
document.getElementById('bambuStatusMessage').innerText = 'Error while saving Bambu Credentials.';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('bambuStatusMessage').innerText = 'Error while saving: ' + error.message;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
var spoolmanUrl = "{{spoolmanUrl}}";
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<h1>Spoolman API URL / Bambu Credentials</h1>
|
||||
<label for="spoolmanUrl">Set URL/IP to your Spoolman-Instanz:</label>
|
||||
<input type="text" id="spoolmanUrl" placeholder="http://ip-or-url-of-your-spoolman-instanz:port">
|
||||
<button onclick="checkSpoolmanInstance()">Save Spoolman URL</button>
|
||||
<p id="statusMessage"></p>
|
||||
|
||||
<h2>Bambu Lab Printer Credentials</h2>
|
||||
<div class="bambu-settings">
|
||||
<div class="input-group">
|
||||
<label for="bambuIp">Bambu Drucker IP-Adresse:</label>
|
||||
<input type="text" id="bambuIp" placeholder="192.168.1.xxx" value="{{bambuIp}}">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="bambuSerial">Drucker Seriennummer:</label>
|
||||
<input type="text" id="bambuSerial" placeholder="BBLXXXXXXXX" value="{{bambuSerial}}">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="bambuCode">Access Code:</label>
|
||||
<input type="text" id="bambuCode" placeholder="Access Code vom Drucker" value="{{bambuCode}}">
|
||||
</div>
|
||||
<button onclick="saveBambuCredentials()">Save Bambu Credentials</button>
|
||||
<p id="bambuStatusMessage"></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
308
html/spoolman.js
Normal file
308
html/spoolman.js
Normal file
@ -0,0 +1,308 @@
|
||||
// Globale Variablen
|
||||
let spoolmanUrl = '';
|
||||
let spoolsData = [];
|
||||
|
||||
// Hilfsfunktionen für Datenmanipulation
|
||||
function processSpoolData(data) {
|
||||
return data.map(spool => ({
|
||||
id: spool.id,
|
||||
remaining_weight: spool.remaining_weight,
|
||||
remaining_length: spool.remaining_length,
|
||||
filament: spool.filament,
|
||||
extra: spool.extra
|
||||
}));
|
||||
}
|
||||
|
||||
// Dropdown-Funktionen
|
||||
function populateVendorDropdown(data, selectedSmId = null) {
|
||||
const vendorSelect = document.getElementById("vendorSelect");
|
||||
if (!vendorSelect) {
|
||||
console.error('vendorSelect Element nicht gefunden');
|
||||
return;
|
||||
}
|
||||
const onlyWithoutSmId = document.getElementById("onlyWithoutSmId");
|
||||
if (!onlyWithoutSmId) {
|
||||
console.error('onlyWithoutSmId Element nicht gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Separate Objekte für alle Hersteller und gefilterte Hersteller
|
||||
const allVendors = {};
|
||||
const filteredVendors = {};
|
||||
|
||||
vendorSelect.innerHTML = '<option value="">Bitte wählen...</option>';
|
||||
|
||||
let vendorIdToSelect = null;
|
||||
let totalSpools = 0;
|
||||
let spoolsWithoutTag = 0;
|
||||
let totalWeight = 0;
|
||||
let totalLength = 0;
|
||||
// Neues Objekt für Material-Gruppierung
|
||||
const materials = {};
|
||||
|
||||
data.forEach(spool => {
|
||||
if (!spool.filament || !spool.filament.vendor) {
|
||||
return;
|
||||
}
|
||||
|
||||
totalSpools++;
|
||||
|
||||
// Material zählen und gruppieren
|
||||
if (spool.filament.material) {
|
||||
const material = spool.filament.material.toUpperCase(); // Normalisierung
|
||||
materials[material] = (materials[material] || 0) + 1;
|
||||
}
|
||||
|
||||
// Addiere Gewicht und Länge
|
||||
if (spool.remaining_weight) {
|
||||
totalWeight += spool.remaining_weight;
|
||||
}
|
||||
if (spool.remaining_length) {
|
||||
totalLength += spool.remaining_length;
|
||||
}
|
||||
|
||||
console.log("Länge gesamt: " + spool.remaining_length);
|
||||
console.log("Gewicht gesamt" + spool.remaining_weight);
|
||||
|
||||
const vendor = spool.filament.vendor;
|
||||
|
||||
const hasValidNfcId = spool.extra &&
|
||||
spool.extra.nfc_id &&
|
||||
spool.extra.nfc_id !== '""' &&
|
||||
spool.extra.nfc_id !== '"\\"\\"\\""';
|
||||
|
||||
if (!hasValidNfcId) {
|
||||
spoolsWithoutTag++;
|
||||
}
|
||||
|
||||
// Alle Hersteller sammeln
|
||||
if (!allVendors[vendor.id]) {
|
||||
allVendors[vendor.id] = vendor.name;
|
||||
}
|
||||
|
||||
// Gefilterte Hersteller für Dropdown
|
||||
if (!filteredVendors[vendor.id]) {
|
||||
if (!onlyWithoutSmId.checked || !hasValidNfcId) {
|
||||
filteredVendors[vendor.id] = vendor.name;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Dropdown mit gefilterten Herstellern befüllen
|
||||
Object.entries(filteredVendors).forEach(([id, name]) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = id;
|
||||
option.textContent = name;
|
||||
vendorSelect.appendChild(option);
|
||||
});
|
||||
|
||||
document.getElementById("totalSpools").textContent = totalSpools;
|
||||
document.getElementById("spoolsWithoutTag").textContent = spoolsWithoutTag;
|
||||
// Zeige die Gesamtzahl aller Hersteller an
|
||||
document.getElementById("totalVendors").textContent = Object.keys(allVendors).length;
|
||||
|
||||
// Neue Statistiken hinzufügen
|
||||
document.getElementById("totalWeight").textContent = (totalWeight / 1000).toFixed(2);
|
||||
document.getElementById("totalLength").textContent = (totalLength / 1000).toFixed(2);
|
||||
|
||||
// Material-Statistiken zum DOM hinzufügen
|
||||
const materialsList = document.getElementById("materialsList");
|
||||
materialsList.innerHTML = '';
|
||||
Object.entries(materials)
|
||||
.sort(([,a], [,b]) => b - a) // Sortiere nach Anzahl absteigend
|
||||
.forEach(([material, count]) => {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = `${material}: ${count} ${count === 1 ? 'Spule' : 'Spulen'}`;
|
||||
materialsList.appendChild(li);
|
||||
});
|
||||
|
||||
if (vendorIdToSelect) {
|
||||
vendorSelect.value = vendorIdToSelect;
|
||||
updateFilamentDropdown(selectedSmId);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFilamentDropdown(selectedSmId = null) {
|
||||
const vendorId = document.getElementById("vendorSelect").value;
|
||||
const dropdownContentInner = document.getElementById("filament-dropdown-content");
|
||||
const filamentSection = document.getElementById("filamentSection");
|
||||
const onlyWithoutSmId = document.getElementById("onlyWithoutSmId").checked;
|
||||
const selectedText = document.getElementById("selected-filament");
|
||||
const selectedColor = document.getElementById("selected-color");
|
||||
|
||||
dropdownContentInner.innerHTML = '';
|
||||
selectedText.textContent = "Bitte wählen...";
|
||||
selectedColor.style.backgroundColor = '#FFFFFF';
|
||||
|
||||
if (vendorId) {
|
||||
const filteredFilaments = spoolsData.filter(spool => {
|
||||
const hasValidNfcId = spool.extra &&
|
||||
spool.extra.nfc_id &&
|
||||
spool.extra.nfc_id !== '""' &&
|
||||
spool.extra.nfc_id !== '"\\"\\"\\""';
|
||||
|
||||
return spool.filament.vendor.id == vendorId &&
|
||||
(!onlyWithoutSmId || !hasValidNfcId);
|
||||
});
|
||||
|
||||
filteredFilaments.forEach(spool => {
|
||||
const option = document.createElement("div");
|
||||
option.className = "dropdown-option";
|
||||
option.setAttribute("data-value", spool.filament.id);
|
||||
option.setAttribute("data-nfc-id", spool.extra.nfc_id || "");
|
||||
|
||||
const colorHex = spool.filament.color_hex || 'FFFFFF';
|
||||
option.innerHTML = `
|
||||
<div class="option-color" style="background-color: #${colorHex}"></div>
|
||||
<span>${spool.id} | ${spool.filament.name} (${spool.filament.material})</span>
|
||||
`;
|
||||
|
||||
option.onclick = () => selectFilament(spool);
|
||||
dropdownContentInner.appendChild(option);
|
||||
});
|
||||
|
||||
filamentSection.classList.remove("hidden");
|
||||
} else {
|
||||
filamentSection.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function selectFilament(spool) {
|
||||
const selectedColor = document.getElementById("selected-color");
|
||||
const selectedText = document.getElementById("selected-filament");
|
||||
const dropdownContent = document.getElementById("filament-dropdown-content");
|
||||
|
||||
selectedColor.style.backgroundColor = `#${spool.filament.color_hex || 'FFFFFF'}`;
|
||||
selectedText.textContent = `${spool.id} | ${spool.filament.name} (${spool.filament.material})`;
|
||||
dropdownContent.classList.remove("show");
|
||||
|
||||
document.dispatchEvent(new CustomEvent('filamentSelected', {
|
||||
detail: spool
|
||||
}));
|
||||
}
|
||||
|
||||
// Initialisierung und Event-Handler
|
||||
async function initSpoolman() {
|
||||
try {
|
||||
const response = await fetch('/api/url');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.spoolman_url) {
|
||||
throw new Error('spoolman_url nicht in der Antwort gefunden');
|
||||
}
|
||||
|
||||
spoolmanUrl = data.spoolman_url;
|
||||
|
||||
const fetchedData = await fetchSpoolData();
|
||||
spoolsData = processSpoolData(fetchedData);
|
||||
|
||||
document.dispatchEvent(new CustomEvent('spoolDataLoaded', {
|
||||
detail: spoolsData
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Initialisieren von Spoolman:', error);
|
||||
document.dispatchEvent(new CustomEvent('spoolmanError', {
|
||||
detail: { message: error.message }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSpoolData() {
|
||||
try {
|
||||
if (!spoolmanUrl) {
|
||||
throw new Error('Spoolman URL ist nicht initialisiert');
|
||||
}
|
||||
|
||||
const response = await fetch(`${spoolmanUrl}/api/v1/spool`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Spulen-Daten:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// Exportiere Funktionen
|
||||
window.getSpoolData = () => spoolsData;
|
||||
window.reloadSpoolData = initSpoolman;
|
||||
window.populateVendorDropdown = populateVendorDropdown;
|
||||
window.updateFilamentDropdown = updateFilamentDropdown;
|
||||
window.toggleFilamentDropdown = () => {
|
||||
const content = document.getElementById("filament-dropdown-content");
|
||||
content.classList.toggle("show");
|
||||
};
|
||||
*/
|
||||
|
||||
// Event Listener
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initSpoolman();
|
||||
|
||||
const vendorSelect = document.getElementById('vendorSelect');
|
||||
if (vendorSelect) {
|
||||
vendorSelect.addEventListener('change', () => updateFilamentDropdown());
|
||||
}
|
||||
|
||||
const onlyWithoutSmId = document.getElementById('onlyWithoutSmId');
|
||||
if (onlyWithoutSmId) {
|
||||
onlyWithoutSmId.addEventListener('change', () => {
|
||||
populateVendorDropdown(spoolsData);
|
||||
updateFilamentDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('spoolDataLoaded', (event) => {
|
||||
populateVendorDropdown(event.detail);
|
||||
});
|
||||
|
||||
window.onclick = function(event) {
|
||||
if (!event.target.closest('.custom-dropdown')) {
|
||||
const dropdowns = document.getElementsByClassName("dropdown-content");
|
||||
for (let dropdown of dropdowns) {
|
||||
dropdown.classList.remove("show");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const refreshButton = document.getElementById('refreshSpoolman');
|
||||
if (refreshButton) {
|
||||
refreshButton.addEventListener('click', async () => {
|
||||
try {
|
||||
refreshButton.disabled = true;
|
||||
refreshButton.textContent = 'Wird aktualisiert...';
|
||||
await initSpoolman();
|
||||
refreshButton.textContent = 'Refresh Spoolman';
|
||||
} finally {
|
||||
refreshButton.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Exportiere Funktionen
|
||||
window.getSpoolData = () => spoolsData;
|
||||
window.setSpoolData = (data) => { spoolsData = data; };
|
||||
window.reloadSpoolData = initSpoolman;
|
||||
window.populateVendorDropdown = populateVendorDropdown;
|
||||
window.updateFilamentDropdown = updateFilamentDropdown;
|
||||
window.toggleFilamentDropdown = () => {
|
||||
const content = document.getElementById("filament-dropdown-content");
|
||||
content.classList.toggle("show");
|
||||
};
|
||||
|
||||
// Event Listener für Click außerhalb Dropdown
|
||||
window.onclick = function(event) {
|
||||
if (!event.target.closest('.custom-dropdown')) {
|
||||
const dropdowns = document.getElementsByClassName("dropdown-content");
|
||||
for (let dropdown of dropdowns) {
|
||||
dropdown.classList.remove("show");
|
||||
}
|
||||
}
|
||||
};
|
901
html/style.css
Normal file
901
html/style.css
Normal file
@ -0,0 +1,901 @@
|
||||
/* Allgemeine Stile */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f8f9fa;
|
||||
color: #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 40px; /* Anpassen an die Navbar-Höhe */
|
||||
width: auto;
|
||||
margin-right: 15px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* Navigationsleiste */
|
||||
.navbar {
|
||||
background-color: #007bff;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center; /* Zentriert die Navigation */
|
||||
padding: 10px 0;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.navbar a {
|
||||
display: inline-block;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 14px 20px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
transition: background 0.3s, color 0.3s;
|
||||
cursor: pointer !important; /* Wichtig: cursor-Definition für Nav-Links */
|
||||
}
|
||||
|
||||
.navbar a:hover {
|
||||
background-color: #0056b3;
|
||||
color: #fff;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
/* Inhalt */
|
||||
.container {
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 20px; /* Platz für die fixe Navbar */
|
||||
}
|
||||
|
||||
/* Überschriften */
|
||||
h1 {
|
||||
color: #007bff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #007bff;
|
||||
font-size: 24px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Formulare */
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="submit"] {
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
border-color: #007bff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
input[type="submit"]:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button {
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
/* Statusnachricht */
|
||||
#statusMessage {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #8cc4fd;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 30px;
|
||||
text-align: left;
|
||||
}
|
||||
.feature {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
margin: 0 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.feature h3 {
|
||||
font-size: 1.4rem;
|
||||
margin-bottom: 10px;
|
||||
color: #007bff;
|
||||
}
|
||||
.feature p {
|
||||
font-size: 1rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Karten-Stil für optische Trennung */
|
||||
.card {
|
||||
background: #f9f9f9;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Versteckte Elemente */
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Dropdown-Stil */
|
||||
.styled-select {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
font-size: 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23007bff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 15px center;
|
||||
background-size: 15px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.styled-select:hover {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 3px 6px rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
.styled-select:focus {
|
||||
border-color: #007bff;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.styled-select:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* NFC-Status */
|
||||
.nfc-status {
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.nfc-success {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.nfc-error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* Füge diese neuen Styles zu deiner style.css hinzu */
|
||||
|
||||
.three-column-layout {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex: 1;
|
||||
min-width: 0; /* Verhindert Überlauf bei flex-Elementen */
|
||||
}
|
||||
|
||||
.feature-box {
|
||||
background: white;
|
||||
padding: 5px 20px 20px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.feature-box h2 {
|
||||
color: #007bff;
|
||||
font-size: 1.4rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.feature-box ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.feature-box ul li {
|
||||
padding: 8px 5px 5px 5px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 95%;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding-top: 60px;
|
||||
padding-bottom: 20px;;
|
||||
}
|
||||
|
||||
.tray {
|
||||
background: #ffffff;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
border-left: 4px solid #ffffff;
|
||||
}
|
||||
|
||||
.tray p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.tray b {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.three-column-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.column {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.nfc-status-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.status-circle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
border: 2px solid #ccc;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.status-circle.success {
|
||||
background-color: #28a745;
|
||||
border-color: #218838;
|
||||
}
|
||||
|
||||
.status-circle.error {
|
||||
background-color: #dc3545;
|
||||
border-color: #c82333;
|
||||
}
|
||||
|
||||
.nfc-data {
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nfc-data p {
|
||||
margin: 5px 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.nfc-status-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 10px;
|
||||
background-color: #fff3f3;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
.info-message {
|
||||
padding: 10px;
|
||||
background-color: #fff3f3;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #39d82e;
|
||||
}
|
||||
|
||||
.nfc-header {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 40px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.nfc-header h2 {
|
||||
margin: 0;
|
||||
grid-column: 2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nfc-header .status-circle {
|
||||
grid-column: 3;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background-color: #fff3f3;
|
||||
border: 1px solid #dc3545;
|
||||
border-radius: 4px;
|
||||
padding: 10px 15px;
|
||||
margin: 15px auto;
|
||||
color: #dc3545;
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-width: 90%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.connection-status.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(220, 53, 69, 0.2);
|
||||
border-top-color: #dc3545;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.connection-status.visible .spinner {
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.connection-status.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nfc-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center; /* Zentriert das div */
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
/* Filament Select Styling */
|
||||
#filamentSelect {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
font-size: 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23007bff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 15px center;
|
||||
background-size: 15px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
#filamentSelect:hover {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 3px 6px rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
#filamentSelect:focus {
|
||||
border-color: #007bff;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
#filamentSelect option {
|
||||
padding: 8px 15px;
|
||||
font-size: 16px;
|
||||
background-color: #fff;
|
||||
color: #000; /* Standard Textfarbe für alles außer dem Farbblock */
|
||||
}
|
||||
|
||||
#filamentSelect option::first-letter {
|
||||
font-size: 16px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#filamentSelect option::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Color Box im Select */
|
||||
.color-box {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 1px solid #333;
|
||||
border-radius: 2px;
|
||||
margin-right: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#filamentSelect option span {
|
||||
display: inline-block;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#filamentSelect option span:first-child {
|
||||
margin-right: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Filament Select Option Styling */
|
||||
#filamentSelect option span.color-circle {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
border: 1px solid #333;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Custom Dropdown */
|
||||
.custom-dropdown {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
cursor: default; /* Container selbst soll normalen Cursor haben */
|
||||
}
|
||||
|
||||
.dropdown-button {
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dropdown-button:hover {
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.selected-color {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #333;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
margin-left: auto;
|
||||
color: #007bff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
margin-top: 4px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.dropdown-content.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-option {
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dropdown-option:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.option-color {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #333;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 25px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
z-index: 1000;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.notification.success {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.notification.fade-out {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Neue Styles für die Statistiken */
|
||||
.statistics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.statistics-column {
|
||||
background: #f8f9fa;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.statistics-column h3 {
|
||||
color: #007bff;
|
||||
margin-bottom: 5px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.statistics-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.statistics-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 5px 0 5px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.statistics-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
/* Responsive Design Anpassung */
|
||||
@media (max-width: 768px) {
|
||||
.statistics-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.statistics-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.refresh-button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.refresh-button:active {
|
||||
background-color: #004494;
|
||||
}
|
||||
|
||||
.spools-info {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.spool-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.spool-stat .stat-label {
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.spool-stat .stat-value {
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
/* Buttons und klickbare Elemente */
|
||||
button,
|
||||
input[type="submit"],
|
||||
.dropdown-button,
|
||||
.dropdown-option,
|
||||
.refresh-button,
|
||||
.btn,
|
||||
.styled-select,
|
||||
select,
|
||||
a {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
/* Disabled Zustände */
|
||||
button:disabled,
|
||||
input[type="submit"]:disabled,
|
||||
.btn:disabled,
|
||||
.styled-select:disabled {
|
||||
cursor: not-allowed !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Schreib-Button */
|
||||
#writeNfcButton {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
#writeNfcButton.writing {
|
||||
background-color: #ffc107;
|
||||
color: black;
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
#writeNfcButton.success {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
#writeNfcButton.error {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
0% { content: ""; }
|
||||
33% { content: "."; }
|
||||
66% { content: ".."; }
|
||||
100% { content: "..."; }
|
||||
}
|
||||
|
||||
#writeNfcButton.writing::after {
|
||||
content: "...";
|
||||
animation: dots 1s steps(3, end) infinite;
|
||||
}
|
||||
|
||||
.reboot-button {
|
||||
background-color: #ff0000;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reboot-button:hover {
|
||||
background-color: #cc0000;
|
||||
}
|
||||
|
||||
/* Bambu Settings Erweiterung */
|
||||
.bambu-settings {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
max-width: 400px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.bambu-settings .input-group {
|
||||
margin-bottom: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bambu-settings .input-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.bambu-settings .input-group input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#bambuStatusMessage {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.tray {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spool-button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
99
html/waage.html
Normal file
99
html/waage.html
Normal file
@ -0,0 +1,99 @@
|
||||
{{header}}
|
||||
<div class="content">
|
||||
<h1>Scale Configuration Page</h1>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Sacle Calibration</h5>
|
||||
<button id="calibrateBtn" class="btn btn-primary">Calibrate Scale</button>
|
||||
<button id="tareBtn" class="btn btn-secondary">Tare Scale</button>
|
||||
<div id="statusMessage" class="mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Neue Kalibrierungskarte -->
|
||||
<div id="calibrationCard" class="card mt-3" style="display: none;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Calibration done</h5>
|
||||
<p>Please follow these steps:</p>
|
||||
<ol>
|
||||
<li>Make sure the scale is empty</li>
|
||||
<li>Have a 500g calibration weight ready</li>
|
||||
<li>Click on "Start Calibration"</li>
|
||||
<li>Follow the further instructions</li>
|
||||
</ol>
|
||||
<ol>
|
||||
<li>Step 1: Empty the scale</li>
|
||||
<li>Step 2: Place the 500g weight on the scale</li>
|
||||
<li>Step 3: Remove weight from Scale</li>
|
||||
</ol>
|
||||
<button id="startCalibrationBtn" class="btn btn-danger">Start Calibration</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let ws = null;
|
||||
const statusMessage = document.getElementById('statusMessage');
|
||||
|
||||
function connectWebSocket() {
|
||||
ws = new WebSocket(`ws://${window.location.hostname}/ws`);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket verbunden');
|
||||
statusMessage.innerHTML = 'Scale connected';
|
||||
enableButtons(true);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket getrennt');
|
||||
statusMessage.innerHTML = 'Scale connection lost';
|
||||
enableButtons(false);
|
||||
setTimeout(connectWebSocket, 2000);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'scale') {
|
||||
if (data.payload === 'success') {
|
||||
statusMessage.innerHTML = 'Well done';
|
||||
statusMessage.className = 'alert alert-success';
|
||||
} else if (data.payload === 'error') {
|
||||
statusMessage.innerHTML = 'Error while action';
|
||||
statusMessage.className = 'alert alert-danger';
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function enableButtons(enabled) {
|
||||
document.getElementById('calibrateBtn').disabled = !enabled;
|
||||
document.getElementById('tareBtn').disabled = !enabled;
|
||||
}
|
||||
|
||||
document.getElementById('calibrateBtn').addEventListener('click', () => {
|
||||
// Kalibrierungskarte anzeigen
|
||||
document.getElementById('calibrationCard').style.display = 'block';
|
||||
});
|
||||
|
||||
document.getElementById('startCalibrationBtn').addEventListener('click', () => {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'scale',
|
||||
payload: 'calibrate'
|
||||
}));
|
||||
// Optional: Kalibrierungskarte nach dem Start ausblenden
|
||||
document.getElementById('calibrationCard').style.display = 'none';
|
||||
});
|
||||
|
||||
document.getElementById('tareBtn').addEventListener('click', () => {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'scale',
|
||||
payload: 'tare'
|
||||
}));
|
||||
});
|
||||
|
||||
// WebSocket-Verbindung beim Laden der Seite initiieren
|
||||
connectWebSocket();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
12
html/wifi.html
Normal file
12
html/wifi.html
Normal file
@ -0,0 +1,12 @@
|
||||
{{header}}
|
||||
<div class="content">
|
||||
<h1>WiFi Configuration Page</h1>
|
||||
<form action="/setToken" method="post">
|
||||
<label for="deviceToken">Device Token:</label><br>
|
||||
<input type="text" id="deviceToken" name="deviceToken"><br>
|
||||
<input type="submit" value="Set Token">
|
||||
</form>
|
||||
<p>Configure your WiFi settings here.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user