makerstash/client/features.js

332 lines
9.5 KiB
JavaScript

/**
* New Features Implementation
* - Filament/Resin Cost Calculator
* - Full-text Search Enhancement
* - License Management
* - Printer Integration (Bambu)
* - Dark/Light Theme Toggle
*/
/**
* Show Printer Settings Modal
*/
function showPrinterSettings() {
loadPrinters();
openModal('printerSettingsModal');
}
/**
* Load user's connected printers
*/
function loadPrinters() {
fetch(`${API_BASE}/printers/printers`, {
headers: { 'Authorization': `Bearer ${authToken}` }
})
.then(res => res.json())
.then(data => {
displayPrinters(data.printers || []);
})
.catch(err => console.error('Error loading printers:', err));
}
/**
* Display connected printers
*/
function displayPrinters(printers) {
const printerList = document.getElementById('printerList');
if (printers.length === 0) {
printerList.innerHTML = '<p class="text-muted">No printers connected yet.</p>';
return;
}
printerList.innerHTML = printers.map(printer => `
<div class="printer-item">
<h4>${printer.printer_name || 'Unnamed Printer'}</h4>
<p><strong>Type:</strong> ${printer.printer_type}</p>
<p><strong>Serial:</strong> ${printer.serial_number}</p>
<p><strong>Model:</strong> ${printer.model_name || 'N/A'}</p>
<div class="printer-actions">
<button class="btn btn-sm btn-secondary" onclick="refreshPrinterStatus(${printer.id})">
<i class="fas fa-sync"></i> Refresh
</button>
<button class="btn btn-sm btn-danger" onclick="removePrinter(${printer.id})">
<i class="fas fa-trash"></i> Remove
</button>
</div>
</div>
`).join('');
}
/**
* Add new Bambu printer
*/
function handleAddPrinter(event) {
event.preventDefault();
const printerName = document.getElementById('printerName').value;
const printerSerial = document.getElementById('printerSerial').value;
const printerModel = document.getElementById('printerModel').value;
const printerToken = document.getElementById('printerToken').value;
fetch(`${API_BASE}/printers/bambu/connect`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
printerName,
serialNumber: printerSerial,
modelName: printerModel,
accessToken: printerToken
})
})
.then(res => res.json())
.then(data => {
if (data.printerConnected) {
showNotification('Printer connected successfully!', 'success');
document.querySelector('#printerSettingsModal form').reset();
loadPrinters();
} else {
showNotification(data.error || 'Failed to connect printer', 'error');
}
})
.catch(err => {
showNotification('Error connecting printer: ' + err.message, 'error');
});
}
/**
* Remove connected printer
*/
function removePrinter(printerId) {
if (!confirm('Are you sure you want to disconnect this printer?')) {
return;
}
fetch(`${API_BASE}/printers/printers/${printerId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${authToken}` }
})
.then(res => res.json())
.then(data => {
showNotification('Printer disconnected', 'success');
loadPrinters();
})
.catch(err => showNotification('Error removing printer: ' + err.message, 'error'));
}
/**
* Refresh printer status
*/
function refreshPrinterStatus(printerId) {
fetch(`${API_BASE}/printers/bambu/${printerId}/status`, {
headers: { 'Authorization': `Bearer ${authToken}` }
})
.then(res => res.json())
.then(data => {
showNotification('Printer status: ' + JSON.stringify(data.data || data.error), 'info');
})
.catch(err => showNotification('Error fetching printer status', 'error'));
}
/**
* Show cost calculator modal
*/
function showCostCalculator() {
openModal('costCalculatorModal');
updateCostEstimate();
}
/**
* Update cost estimate for selected models
*/
function updateCostEstimate() {
const selectedModels = Array.from(document.querySelectorAll('.model-card.selected')).map(card => ({
id: parseInt(card.dataset.modelId),
name: card.querySelector('.model-name').textContent,
file_size: parseInt(card.dataset.fileSize)
}));
const materialType = document.getElementById('materialType')?.value || 'pla';
const costResults = document.getElementById('costResults');
if (selectedModels.length === 0) {
costResults.innerHTML = '<p class="text-muted">Select models from the grid to see cost estimates</p>';
return;
}
const modelIds = selectedModels.map(m => m.id);
fetch(`${API_BASE}/models/batch/cost`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ modelIds, materialType })
})
.then(res => res.json())
.then(data => {
displayCostResults(data);
})
.catch(err => {
costResults.innerHTML = `<p class="text-muted">Error calculating costs: ${err.message}</p>`;
});
}
/**
* Display cost calculation results
*/
function displayCostResults(data) {
const costResults = document.getElementById('costResults');
const cardsHtml = data.models.map(model => `
<div class="cost-card">
<h4 title="${model.name}">${model.name}</h4>
<div class="cost-detail">
<span>Weight:</span>
<span class="value">${model.weight.toFixed(1)}g</span>
</div>
<div class="cost-detail">
<span>Units (${model.material}):</span>
<span class="value">${model.units.toFixed(3)}</span>
</div>
<div class="cost-detail">
<span>Cost per Unit:</span>
<span class="value">$${model.costPerUnit.toFixed(2)}</span>
</div>
<div class="cost-detail total">
<span>Estimated Cost:</span>
<span>$${model.estimatedCost.toFixed(2)}</span>
</div>
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.5rem;">
Confidence: ${model.confidence}
</div>
</div>
`).join('');
costResults.innerHTML = cardsHtml + `
<div class="cost-summary" style="grid-column: 1 / -1;">
<h3>$${data.totalCost.toFixed(2)}</h3>
<p>Total Estimated Cost for ${data.models.length} model(s)</p>
<p style="font-size: 0.8rem; margin-top: 0.5rem;">Average: $${data.averageCost.toFixed(2)} per model</p>
</div>
`;
}
/**
* Calculate cost for single model
*/
function calculateModelCost(modelId, fileSize, materialType = 'pla') {
fetch(`${API_BASE}/models/${modelId}/cost?materialType=${materialType}`, {
headers: { 'Authorization': `Bearer ${authToken}` }
})
.then(res => res.json())
.then(data => {
return data;
})
.catch(err => console.error('Error calculating cost:', err));
}
/**
* Add cost badge to model card
*/
function addCostBadge(modelCard, cost) {
const existing = modelCard.querySelector('.model-cost-badge');
if (existing) existing.remove();
const badge = document.createElement('div');
badge.className = 'model-cost-badge';
badge.textContent = `$${cost.toFixed(2)}`;
modelCard.querySelector('.model-info')?.appendChild(badge);
}
/**
* Enhanced search with full-text support
*/
function handleAdvancedSearch(query) {
const params = new URLSearchParams({
search: query,
license: document.getElementById('filterLicense')?.value || '',
fileType: document.getElementById('filterFileType')?.value || '',
hasSupports: document.getElementById('filterSupports')?.checked || false,
sortBy: document.getElementById('sortBy')?.value || 'created_at',
sortOrder: document.getElementById('sortOrder')?.value || 'DESC'
});
fetch(`${API_BASE}/models?${params}`, {
headers: { 'Authorization': `Bearer ${authToken}` }
})
.then(res => res.json())
.then(data => {
displayModels(data.models);
})
.catch(err => console.error('Error searching:', err));
}
/**
* Apply all filters including license
*/
function applyFilters() {
const searchTerm = document.getElementById('searchInput')?.value || '';
handleAdvancedSearch(searchTerm);
}
/**
* Display license in model card
*/
function displayModelLicense(modelCard, license) {
const licenseEl = document.createElement('p');
licenseEl.style.fontSize = '0.75rem';
licenseEl.style.color = 'var(--text-secondary)';
licenseEl.textContent = `📜 ${license}`;
modelCard.querySelector('.model-info')?.appendChild(licenseEl);
}
/**
* Theme toggle
*/
function toggleTheme() {
if (typeof setTheme === 'function') {
const current = localStorage.getItem('theme') || 'light';
const newTheme = current === 'light' ? 'dark' : 'light';
setTheme(newTheme);
}
}
/**
* Update model card with additional info (license, cost)
*/
function enhanceModelCard(modelCard, model) {
// Add license info
if (model.license && model.license !== 'Unknown') {
displayModelLicense(modelCard, model.license);
}
// Add cost estimate for selected material
const defaultMaterial = localStorage.getItem('defaultMaterial') || 'pla';
// Note: Cost will be added when user interacts or opens calculator
}
// Auto-load materials on page init
document.addEventListener('DOMContentLoaded', () => {
// Load materials for the cost calculator
fetch(`${API_BASE}/models/config/materials`)
.then(res => res.json())
.then(data => {
console.log('Available materials:', data.materials);
})
.catch(err => console.error('Error loading materials:', err));
});
export {
showPrinterSettings,
loadPrinters,
showCostCalculator,
updateCostEstimate,
handleAdvancedSearch,
applyFilters,
toggleTheme
};