// Additional Features for MakerStash
// This file contains all the new feature implementations
// State for bulk selection
let selectedModels = new Set();
let cachedModels = []; // Store models for later reference
// Filter and Sort Functions
function applyFilters() {
const fileType = document.getElementById('filterFileType').value;
const hasSupports = document.getElementById('filterSupports').checked;
const sortBy = document.getElementById('sortBy').value;
const sortOrder = document.getElementById('sortOrder').value;
const searchTerm = document.getElementById('searchInput').value;
loadAllModels(searchTerm, '', '', fileType, hasSupports, sortBy, sortOrder);
}
function clearFilters() {
document.getElementById('filterFileType').value = '';
document.getElementById('filterSupports').checked = false;
document.getElementById('sortBy').value = 'created_at';
document.getElementById('sortOrder').value = 'DESC';
document.getElementById('searchInput').value = '';
loadAllModels();
}
// Update loadAllModels to accept filter parameters
const originalLoadAllModels = window.loadAllModels;
window.loadAllModels = async function(search = '', tag = '', collection = '', fileType = '', hasSupports = false, sortBy = 'created_at', sortOrder = 'DESC') {
const grid = document.getElementById('modelsGrid');
grid.innerHTML = '
';
try {
let url = `${API_BASE}/models?`;
if (search) url += `search=${encodeURIComponent(search)}&`;
if (tag) url += `tag=${encodeURIComponent(tag)}&`;
if (collection) url += `collection=${encodeURIComponent(collection)}&`;
if (fileType) url += `fileType=${encodeURIComponent(fileType)}&`;
if (hasSupports) url += `hasSupports=true&`;
url += `sortBy=${sortBy}&sortOrder=${sortOrder}`;
const response = await fetch(url, {
headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}
});
const data = await response.json();
if (!response.ok) {
if (response.status === 401) {
grid.innerHTML = 'Please log in to view models
';
return;
}
throw new Error(data.error || 'Failed to load models');
}
if (data.models && data.models.length > 0) {
displayModelsWithSelection(data.models);
} else {
grid.innerHTML = '';
}
} catch (error) {
grid.innerHTML = `Error loading models: ${error.message}
`;
console.error('Error loading models:', error);
}
};
// Display models with selection checkboxes
function displayModelsWithSelection(models) {
cachedModels = models; // Cache the models
const grid = document.getElementById('modelsGrid');
grid.innerHTML = '';
models.forEach(model => {
const card = document.createElement('div');
card.className = 'model-card';
if (selectedModels.has(model.id)) {
card.classList.add('selected');
}
card.onclick = (e) => {
if (!e.target.classList.contains('model-checkbox')) {
showModelDetails(model.id);
}
};
const fileIcon = getFileIcon(model.file_type);
const fileSize = formatFileSize(model.file_size);
const createdDate = new Date(model.created_at).toLocaleDateString();
const previewContent = model.preview_image
? `
`
: ``;
const has3DViewer = ['.stl', '.obj', '.3mf'].includes(model.file_type);
card.innerHTML = `
${previewContent}
${has3DViewer ? `
` : ''}
${escapeHtml(model.name)}
${escapeHtml(model.description || 'No description')}
${fileSize}
${createdDate}
${model.tags && model.tags.length > 0 ? `
${model.tags.map(tag => `${escapeHtml(tag)}`).join('')}
` : ''}
`;
grid.appendChild(card);
});
}
// Bulk Selection Functions
function toggleModelSelection(modelId) {
if (selectedModels.has(modelId)) {
selectedModels.delete(modelId);
} else {
selectedModels.add(modelId);
}
updateBulkToolbar();
updateSelectedCards();
}
function updateBulkToolbar() {
const toolbar = document.getElementById('bulkToolbar');
const count = document.getElementById('bulkSelectionCount');
if (selectedModels.size > 0) {
toolbar.style.display = 'block';
count.textContent = `${selectedModels.size} selected`;
} else {
toolbar.style.display = 'none';
}
}
function updateSelectedCards() {
document.querySelectorAll('.model-card').forEach(card => {
const checkbox = card.querySelector('.model-checkbox');
if (checkbox && checkbox.checked) {
card.classList.add('selected');
} else {
card.classList.remove('selected');
}
});
}
function clearSelection() {
selectedModels.clear();
document.querySelectorAll('.model-checkbox').forEach(cb => cb.checked = false);
updateBulkToolbar();
updateSelectedCards();
}
// Bulk Operations
async function bulkAddTags() {
if (selectedModels.size === 0) return;
// Populate and show modal
showModal('bulkTagsModal');
}
async function handleBulkAddTags(event) {
event.preventDefault();
const tagsInput = document.getElementById('bulkTagsInput').value;
const tags = tagsInput.split(',').map(t => t.trim()).filter(t => t);
if (tags.length === 0) {
showNotification('Please enter at least one tag', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/bulk/tag`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
modelIds: Array.from(selectedModels),
tags: tags
})
});
if (response.ok) {
closeModal('bulkTagsModal');
showNotification('Tags added successfully!', 'success');
document.getElementById('bulkTagsInput').value = '';
clearSelection();
loadAllModels();
loadTags();
} else {
const data = await response.json();
showNotification(data.error || 'Failed to add tags', 'error');
}
} catch (error) {
showNotification('Failed to add tags: ' + error.message, 'error');
}
}
async function bulkMoveToCollection() {
if (selectedModels.size === 0) return;
// Populate collection dropdown
const select = document.getElementById('bulkMoveCollection');
select.innerHTML = '' +
allCollections.map(c => ``).join('');
showModal('bulkMoveModal');
}
async function handleBulkMove(event) {
event.preventDefault();
const collectionId = document.getElementById('bulkMoveCollection').value || null;
try {
const response = await fetch(`${API_BASE}/bulk/move`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
modelIds: Array.from(selectedModels),
collectionId: collectionId
})
});
if (response.ok) {
closeModal('bulkMoveModal');
showNotification('Models moved successfully!', 'success');
clearSelection();
loadAllModels();
} else {
const data = await response.json();
showNotification(data.error || 'Failed to move models', 'error');
}
} catch (error) {
showNotification('Failed to move models: ' + error.message, 'error');
}
}
async function bulkDelete() {
if (selectedModels.size === 0) return;
if (!confirm(`Are you sure you want to delete ${selectedModels.size} models? This cannot be undone.`)) {
return;
}
try {
const response = await fetch(`${API_BASE}/bulk/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
modelIds: Array.from(selectedModels)
})
});
if (response.ok) {
showNotification('Models deleted successfully!', 'success');
clearSelection();
loadAllModels();
} else {
const data = await response.json();
showNotification(data.error || 'Failed to delete models', 'error');
}
} catch (error) {
showNotification('Failed to delete models: ' + error.message, 'error');
}
}
// Print Queue Functions
async function loadPrintQueue() {
try {
const response = await fetch(`${API_BASE}/print-queue`, {
headers: { 'Authorization': `Bearer ${authToken}` }
});
const data = await response.json();
const countBadge = document.getElementById('queueCount');
if (countBadge) {
countBadge.textContent = data.queue.length;
}
return data.queue;
} catch (error) {
console.error('Error loading print queue:', error);
return [];
}
}
async function showPrintQueueModal() {
showModal('printQueueModal');
const queueList = document.getElementById('printQueueList');
queueList.innerHTML = 'Loading queue...
';
const queue = await loadPrintQueue();
if (queue.length === 0) {
queueList.innerHTML = 'Queue is empty
';
return;
}
queueList.innerHTML = queue.map(item => `
${escapeHtml(item.model_name)}
${item.quantity || 1}
${item.filament_type ? `${item.filament_type.toUpperCase()} (${item.color})` : 'Not specified'}
${item.print_temp ? item.print_temp + '°C' : '—'} / ${item.bed_temp ? item.bed_temp + '°C' : '—'}
${item.print_speed ? item.print_speed.charAt(0).toUpperCase() + item.print_speed.slice(1) : 'Normal'}
${item.support_structure ? item.support_structure.charAt(0).toUpperCase() + item.support_structure.slice(1) : 'None'}
${item.infill_density || 20}%
${item.layer_height || 0.2}mm
${getPriorityLabel(item.priority)}
${item.special_instructions ? `
Notes: ${escapeHtml(item.special_instructions)}
` : ''}
Added: ${new Date(item.added_at).toLocaleDateString()}
`).join('');
}
function getPriorityLabel(priority) {
switch(parseInt(priority)) {
case 20: return 'Urgent';
case 10: return 'High';
case 5: return 'Medium';
default: return 'Low';
}
}
let currentEditPrintJobId = null;
async function editPrintJob(jobId) {
try {
// Get the queue item data
const response = await fetch(`${API_BASE}/print-queue`, {
headers: { 'Authorization': `Bearer ${authToken}` }
});
const data = await response.json();
const job = data.queue.find(q => q.id === jobId);
if (!job) {
showNotification('Job not found', 'error');
return;
}
currentEditPrintJobId = jobId;
// Populate the form
document.getElementById('editPrintModelName').value = job.model_name;
document.getElementById('editPrintQuantity').value = job.quantity || 1;
document.getElementById('editPrintFilamentType').value = job.filament_type || '';
document.getElementById('editPrintColor').value = job.color || '';
document.getElementById('editPrintTemp').value = job.print_temp || '';
document.getElementById('editPrintBedTemp').value = job.bed_temp || '';
document.getElementById('editPrintSpeed').value = job.print_speed || 'normal';
document.getElementById('editPrintSupport').value = job.support_structure || 'none';
document.getElementById('editPrintInfill').value = job.infill_density || 20;
document.getElementById('editPrintLayerHeight').value = job.layer_height || 0.2;
document.getElementById('editPrintNotes').value = job.special_instructions || '';
document.getElementById('editPrintPriority').value = job.priority || 0;
showModal('editPrintJobModal');
} catch (error) {
showNotification('Failed to load job details: ' + error.message, 'error');
console.error('Error:', error);
}
}
async function saveEditPrintJob(event) {
event.preventDefault();
if (!currentEditPrintJobId) {
showNotification('Job ID not set', 'error');
return;
}
const printData = {
quantity: parseInt(document.getElementById('editPrintQuantity').value),
filament_type: document.getElementById('editPrintFilamentType').value,
color: document.getElementById('editPrintColor').value,
print_temp: document.getElementById('editPrintTemp').value ? parseInt(document.getElementById('editPrintTemp').value) : null,
bed_temp: document.getElementById('editPrintBedTemp').value ? parseInt(document.getElementById('editPrintBedTemp').value) : null,
print_speed: document.getElementById('editPrintSpeed').value,
support_structure: document.getElementById('editPrintSupport').value,
infill_density: parseInt(document.getElementById('editPrintInfill').value),
layer_height: parseFloat(document.getElementById('editPrintLayerHeight').value),
special_instructions: document.getElementById('editPrintNotes').value,
priority: parseInt(document.getElementById('editPrintPriority').value)
};
try {
const response = await fetch(`${API_BASE}/print-queue/${currentEditPrintJobId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify(printData)
});
const data = await response.json();
if (!response.ok) {
showNotification('Error saving changes: ' + (data.error || 'Unknown error'), 'error');
return;
}
showNotification('Print job updated successfully!', 'success');
closeModal('editPrintJobModal');
showPrintQueueModal(); // Refresh the queue display
} catch (error) {
showNotification('Failed to save changes: ' + error.message, 'error');
console.error('Error:', error);
}
}
async function removePrintJob() {
if (!currentEditPrintJobId) return;
if (!confirm('Are you sure you want to delete this print job?')) return;
try {
const response = await fetch(`${API_BASE}/print-queue/${currentEditPrintJobId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (!response.ok) {
showNotification('Error deleting job', 'error');
return;
}
showNotification('Print job deleted', 'success');
closeModal('editPrintJobModal');
showPrintQueueModal(); // Refresh the queue display
} catch (error) {
showNotification('Failed to delete job: ' + error.message, 'error');
}
}
async function bulkAddToQueue() {
if (selectedModels.size === 0) return;
// If only one model, show detailed form
if (selectedModels.size === 1) {
const modelId = Array.from(selectedModels)[0];
// Find the model in cached models
const model = cachedModels.find(m => m.id === modelId);
const modelName = model ? model.name : `Model ${modelId}`;
showPrintDetailsModal(modelId, modelName);
return;
}
// For multiple models, add them quickly with basic settings
try {
const promises = Array.from(selectedModels).map(modelId =>
fetch(`${API_BASE}/print-queue`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ modelId, priority: 0 })
})
);
await Promise.all(promises);
showNotification('Models added to print queue!', 'success');
clearSelection();
loadPrintQueue();
} catch (error) {
showNotification('Failed to add to queue: ' + error.message, 'error');
}
}
async function updateQueuePriority(queueId, priority) {
try {
await fetch(`${API_BASE}/print-queue/${queueId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ priority: parseInt(priority) })
});
} catch (error) {
console.error('Error updating priority:', error);
}
}
async function completeQueueItem(queueId) {
try {
await fetch(`${API_BASE}/print-queue/${queueId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ status: 'completed' })
});
showNotification('Marked as completed!', 'success');
showPrintQueueModal();
loadPrintQueue();
} catch (error) {
showNotification('Error: ' + error.message, 'error');
}
}
async function removeFromQueue(queueId) {
try {
await fetch(`${API_BASE}/print-queue/${queueId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${authToken}` }
});
showNotification('Removed from queue', 'success');
showPrintQueueModal();
loadPrintQueue();
} catch (error) {
showNotification('Error: ' + error.message, 'error');
}
}
// Export/Import Functions
async function exportAllModels() {
try {
const response = await fetch(`${API_BASE}/export/all`, {
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `makerstash-export-${Date.now()}.zip`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showNotification('Export started! Check your downloads.', 'success');
} else {
showNotification('Export failed', 'error');
}
} catch (error) {
showNotification('Export error: ' + error.message, 'error');
}
}
async function bulkExport() {
if (selectedModels.size === 0) return;
try {
const response = await fetch(`${API_BASE}/export/models`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ modelIds: Array.from(selectedModels) })
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `makerstash-models-${Date.now()}.zip`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showNotification('Export started!', 'success');
} else {
showNotification('Export failed', 'error');
}
} catch (error) {
showNotification('Export error: ' + error.message, 'error');
}
}
function showImportModal() {
// Populate collection dropdown
const select = document.getElementById('importCollection');
select.innerHTML = '' +
allCollections.map(c => ``).join('');
showModal('importModal');
}
// Quick add function - creates a reference without file
async function handleQuickImport(event) {
event.preventDefault();
const url = document.getElementById('importUrl').value;
const name = document.getElementById('importName').value;
const description = document.getElementById('importDescription').value;
const creator = document.getElementById('importCreator').value;
const collectionId = document.getElementById('importCollection').value;
const tagsInput = document.getElementById('importTags').value;
const tags = tagsInput ? tagsInput.split(',').map(t => t.trim()).filter(t => t) : [];
// Create a placeholder file entry (we'll create a dummy text file)
const dummyContent = `This is a reference to a model from ${url}\n\nDownload the actual file from the URL above and upload it to replace this reference.`;
const blob = new Blob([dummyContent], { type: 'text/plain' });
const file = new File([blob], 'reference.txt', { type: 'text/plain' });
const formData = new FormData();
formData.append('files', file);
formData.append('name', name);
formData.append('description', description || '');
formData.append('creator', creator || '');
formData.append('source_url', url);
formData.append('collection_id', collectionId || '');
formData.append('is_supported', false);
formData.append('notes', `Reference from ${url}`);
if (tags.length > 0) {
formData.append('tags', JSON.stringify(tags));
}
try {
const response = await fetch(`${API_BASE}/models`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${authToken}` },
body: formData
});
if (response.ok) {
closeModal('importModal');
showNotification('Reference saved! You can now download the file from the URL and upload it.', 'success');
// Clear form
document.getElementById('importUrl').value = '';
document.getElementById('importName').value = '';
document.getElementById('importDescription').value = '';
document.getElementById('importCreator').value = '';
document.getElementById('importTags').value = '';
loadAllModels();
} else {
const data = await response.json();
showNotification(data.error || 'Failed to save reference', 'error');
}
} catch (error) {
showNotification('Error: ' + error.message, 'error');
}
}
// Update auth function to show new sections
const originalUpdateUIForAuth = window.updateUIForAuth;
window.updateUIForAuth = function() {
if (originalUpdateUIForAuth) {
originalUpdateUIForAuth();
}
document.getElementById('loginBtn').style.display = 'none';
document.getElementById('registerBtn').style.display = 'none';
document.getElementById('userMenu').style.display = 'flex';
document.getElementById('username').textContent = currentUser.username;
document.getElementById('uploadBtn').style.display = 'block';
document.getElementById('createCollectionBtn').style.display = 'block';
document.getElementById('printQueueSection').style.display = 'block';
document.getElementById('exportSection').style.display = 'block';
// Load print queue
if (authToken) {
loadPrintQueue();
}
};
// Print Details Modal Functions
let currentPrintModelId = null;
function showPrintDetailsModal(modelId, modelName) {
currentPrintModelId = modelId;
document.getElementById('printModelName').value = modelName;
document.getElementById('printQuantity').value = 1;
document.getElementById('printFilamentType').value = '';
document.getElementById('printColor').value = '';
document.getElementById('printTemp').value = '';
document.getElementById('printBedTemp').value = '';
document.getElementById('printSpeed').value = 'normal';
document.getElementById('printSupport').value = 'none';
document.getElementById('printInfill').value = 20;
document.getElementById('printLayerHeight').value = 0.2;
document.getElementById('printNotes').value = '';
document.getElementById('printPriority').value = 0;
showModal('printDetailsModal');
}
async function savePrintDetails(event) {
event.preventDefault();
if (!currentPrintModelId) {
showNotification('Model ID not set', 'error');
return;
}
const printData = {
modelId: currentPrintModelId,
quantity: parseInt(document.getElementById('printQuantity').value),
filament_type: document.getElementById('printFilamentType').value,
color: document.getElementById('printColor').value,
print_temp: document.getElementById('printTemp').value ? parseInt(document.getElementById('printTemp').value) : null,
bed_temp: document.getElementById('printBedTemp').value ? parseInt(document.getElementById('printBedTemp').value) : null,
print_speed: document.getElementById('printSpeed').value,
support_structure: document.getElementById('printSupport').value,
infill_density: parseInt(document.getElementById('printInfill').value),
layer_height: parseFloat(document.getElementById('printLayerHeight').value),
special_instructions: document.getElementById('printNotes').value,
priority: parseInt(document.getElementById('printPriority').value)
};
try {
const response = await fetch(`${API_BASE}/print-queue`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify(printData)
});
const data = await response.json();
if (!response.ok) {
showNotification('Error adding to queue: ' + (data.error || 'Unknown error'), 'error');
return;
}
showNotification('Model added to print queue with specifications!', 'success');
closeModal('printDetailsModal');
loadPrintQueue();
clearSelection();
} catch (error) {
showNotification('Failed to add to queue: ' + error.message, 'error');
console.error('Error:', error);
}
}
console.log('Additional features loaded');