// 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 = '

Loading models...

'; 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 = '

No models found

'; } } 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 ? `${escapeHtml(model.name)}` : ``; 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');