import { api } from '../api.js'; export class FileList extends HTMLElement { constructor() { super(); this.currentFolderId = null; this.folderPath = []; this.files = []; this.folders = []; this.selectedFiles = new Set(); this.selectedFolders = new Set(); this.boundHandleClick = this.handleClick.bind(this); this.boundHandleDblClick = this.handleDblClick.bind(this); this.boundHandleChange = this.handleChange.bind(this); } async connectedCallback() { this.addEventListener('click', this.boundHandleClick); this.addEventListener('dblclick', this.boundHandleDblClick); this.addEventListener('change', this.boundHandleChange); await this.loadContents(null); } disconnectedCallback() { this.removeEventListener('click', this.boundHandleClick); this.removeEventListener('dblclick', this.boundHandleDblClick); this.removeEventListener('change', this.boundHandleChange); } async loadContents(folderId) { this.currentFolderId = folderId; try { if (folderId) { try { this.folderPath = await api.getFolderPath(folderId); } catch (pathError) { this.folderPath = []; } } else { this.folderPath = []; } this.folders = await api.listFolders(folderId); this.files = await api.listFiles(folderId); this.selectedFiles.clear(); this.selectedFolders.clear(); this.render(); } catch (error) { document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: 'Failed to load folder contents', type: 'error' } })); this.folderPath = []; this.folders = []; this.files = []; this.render(); } } setFiles(files) { this.files = files; this.folders = []; this.selectedFiles.clear(); this.selectedFolders.clear(); this.render(); } render() { const hasSelected = this.selectedFiles.size > 0 || this.selectedFolders.size > 0; const totalItems = this.files.length + this.folders.length; const totalSelected = this.selectedFiles.size + this.selectedFolders.size; const allFilesSelected = this.files.length > 0 && this.selectedFiles.size === this.files.length; const allFoldersSelected = this.folders.length > 0 && this.selectedFolders.size === this.folders.length; const allSelected = totalItems > 0 && allFilesSelected && allFoldersSelected; const someSelected = hasSelected && !allSelected; this.innerHTML = `
${this.folderPath.length > 0 ? ` ` : ''}

Files

${totalItems > 0 ? `
` : ''}
${hasSelected ? `
` : ''}
${this.folders.map(folder => this.renderFolder(folder)).join('')} ${this.files.map(file => this.renderFile(file)).join('')}
`; this.attachListeners(); this.updateIndeterminateState(); } renderFolder(folder) { const isSelected = this.selectedFolders.has(folder.id); const starIcon = folder.is_starred ? '★' : '☆'; // Filled star or empty star const starAction = folder.is_starred ? 'unstar-folder' : 'star-folder'; return `
📁
${folder.name}
`; } renderFile(file) { const isSelected = this.selectedFiles.has(file.id); const icon = this.getFileIcon(file.mime_type); const size = this.formatFileSize(file.size); const starIcon = file.is_starred ? '★' : '☆'; // Filled star or empty star const starAction = file.is_starred ? 'unstar-file' : 'star-file'; return `
${icon}
${file.name}
${size}
`; } getFileIcon(mimeType) { if (mimeType.startsWith('image/')) return '📷'; if (mimeType.startsWith('video/')) return '🎥'; if (mimeType.startsWith('audio/')) return '🎵'; if (mimeType.includes('pdf')) return '📄'; if (mimeType.includes('text')) return '📄'; return '📄'; } isEditableFile(filename, mimeType) { if (mimeType && mimeType.startsWith('text/')) return true; const editableExtensions = [ 'txt', 'md', 'log', 'json', 'js', 'py', 'html', 'css', 'xml', 'yaml', 'yml', 'sh', 'bat', 'ini', 'conf', 'cfg' ]; const extension = filename.split('.').pop().toLowerCase(); return editableExtensions.includes(extension); } formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB'; return (bytes / 1073741824).toFixed(1) + ' GB'; } handleClick(e) { const target = e.target; if (target.classList.contains('breadcrumb-item') && !target.classList.contains('breadcrumb-current')) { const folderId = target.dataset.folderId; const targetFolderId = folderId === 'null' ? null : parseInt(folderId); this.loadContents(targetFolderId); return; } if (target.id === 'upload-btn') { this.dispatchEvent(new CustomEvent('upload-request', { detail: { folderId: this.currentFolderId } })); return; } if (target.id === 'create-folder-btn') { this.handleCreateFolder(); return; } if (target.id === 'clear-selection-btn') { this.clearSelection(); return; } if (target.id === 'batch-delete-btn') { this.handleBatchAction('delete'); return; } if (target.id === 'batch-move-btn') { this.handleBatchAction('move'); return; } if (target.id === 'batch-copy-btn') { this.handleBatchAction('copy'); return; } if (target.id === 'batch-star-btn') { this.handleBatchAction('star'); return; } if (target.id === 'batch-unstar-btn') { this.handleBatchAction('unstar'); return; } if (target.classList.contains('action-btn')) { e.stopPropagation(); const action = target.dataset.action; const id = parseInt(target.dataset.id); this.handleAction(action, id); return; } if (target.classList.contains('select-item')) { e.stopPropagation(); return; } const fileItem = target.closest('.file-item:not(.folder-item)'); if (fileItem) { const fileId = parseInt(fileItem.dataset.fileId); const file = this.files.find(f => f.id === fileId); if (this.isEditableFile(file.name, file.mime_type)) { this.dispatchEvent(new CustomEvent('edit-file', { detail: { file: file }, bubbles: true })); } else { this.dispatchEvent(new CustomEvent('photo-click', { detail: { photo: file }, bubbles: true })); } } } handleDblClick(e) { if (e.target.classList.contains('select-item') || e.target.classList.contains('action-btn') || e.target.classList.contains('breadcrumb-item')) { return; } const folderItem = e.target.closest('.folder-item'); if (folderItem) { const folderId = parseInt(folderItem.dataset.folderId); this.loadContents(folderId); } } handleChange(e) { const target = e.target; if (target.id === 'select-all') { this.toggleSelectAll(target.checked); return; } if (target.classList.contains('select-item')) { const type = target.dataset.type; const id = parseInt(target.dataset.id); this.toggleSelectItem(type, id, target.checked); } } attachListeners() { this.updateBatchActionVisibility(); } toggleSelectItem(type, id, checked) { if (type === 'file') { if (checked) { this.selectedFiles.add(id); } else { this.selectedFiles.delete(id); } } else if (type === 'folder') { if (checked) { this.selectedFolders.add(id); } else { this.selectedFolders.delete(id); } } this.updateSelectionUI(); } toggleSelectAll(checked) { this.selectedFiles.clear(); this.selectedFolders.clear(); if (checked) { this.files.forEach(file => this.selectedFiles.add(file.id)); this.folders.forEach(folder => this.selectedFolders.add(folder.id)); } this.querySelectorAll('.select-item').forEach(checkbox => { checkbox.checked = checked; }); this.updateSelectionUI(); } clearSelection() { this.selectedFiles.clear(); this.selectedFolders.clear(); this.querySelectorAll('.select-item').forEach(checkbox => { checkbox.checked = false; }); this.updateSelectionUI(); } updateSelectionUI() { const hasSelected = this.selectedFiles.size > 0 || this.selectedFolders.size > 0; const totalItems = this.files.length + this.folders.length; const totalSelected = this.selectedFiles.size + this.selectedFolders.size; const allSelected = totalItems > 0 && totalSelected === totalItems; const selectAllCheckbox = this.querySelector('#select-all'); const selectAllLabel = this.querySelector('label[for="select-all"]'); const batchActionsDiv = this.querySelector('.batch-actions'); if (selectAllCheckbox) { selectAllCheckbox.checked = allSelected; this.updateIndeterminateState(); } if (selectAllLabel) { selectAllLabel.textContent = hasSelected ? `${totalSelected} selected` : 'Select all'; } if (hasSelected && !batchActionsDiv) { const container = this.querySelector('.file-list-container'); const header = container.querySelector('.file-list-header'); const batchBar = document.createElement('div'); batchBar.className = 'batch-actions'; batchBar.innerHTML = ` `; header.insertAdjacentElement('afterend', batchBar); } else if (!hasSelected && batchActionsDiv) { batchActionsDiv.remove(); } } updateIndeterminateState() { const selectAllCheckbox = this.querySelector('#select-all'); if (selectAllCheckbox) { const totalItems = this.files.length + this.folders.length; const totalSelected = this.selectedFiles.size + this.selectedFolders.size; const hasSelected = totalSelected > 0; const allSelected = totalItems > 0 && totalSelected === totalItems; selectAllCheckbox.indeterminate = hasSelected && !allSelected; } } updateBatchActionVisibility() { this.updateSelectionUI(); } async handleBatchAction(action) { if ((this.selectedFiles.size === 0 && this.selectedFolders.size === 0)) { document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: 'No items selected for batch operation.', type: 'info' } })); return; } if (!confirm(`Are you sure you want to ${action} ${this.selectedFiles.size + this.selectedFolders.size} items?`)) { return; } try { let targetFolderId = null; if (action === 'move' || action === 'copy') { const folderName = prompt('Enter target folder ID (leave empty for root):'); if (folderName !== null) { targetFolderId = folderName === '' ? null : parseInt(folderName); if (folderName !== '' && isNaN(targetFolderId)) { document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: 'Invalid folder ID.', type: 'error' } })); return; } } else { return; // User cancelled } } if (this.selectedFiles.size > 0) { await api.batchFileOperations(action, Array.from(this.selectedFiles), targetFolderId); } if (this.selectedFolders.size > 0) { await api.batchFolderOperations(action, Array.from(this.selectedFolders), targetFolderId); } document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: `Batch ${action} successful!`, type: 'success' } })); await this.loadContents(this.currentFolderId); // Reload contents } catch (error) { document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: `Batch ${action} failed: ` + error.message, type: 'error' } })); } } triggerCreateFolder() { this.handleCreateFolder(); } async handleCreateFolder() { const name = prompt('Enter folder name:'); if (name) { try { await api.createFolder(name, this.currentFolderId); await this.loadContents(this.currentFolderId); } catch (error) { document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: 'Failed to create folder: ' + error.message, type: 'error' } })); } } } async handleAction(action, id) { try { switch (action) { case 'download': const blob = await api.downloadFile(id); const file = this.files.find(f => f.id === id); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = file.name; a.click(); URL.revokeObjectURL(url); document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: 'File downloaded successfully!', type: 'success' } })); break; case 'rename': const newName = prompt('Enter new name:'); if (newName) { await api.renameFile(id, newName); await this.loadContents(this.currentFolderId); document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: 'File renamed successfully!', type: 'success' } })); } break; case 'delete': if (confirm('Are you sure you want to delete this file?')) { await api.deleteFile(id); await this.loadContents(this.currentFolderId); document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: 'File deleted successfully!', type: 'success' } })); } break; case 'delete-folder': if (confirm('Are you sure you want to delete this folder?')) { await api.deleteFolder(id); await this.loadContents(this.currentFolderId); document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: 'Folder deleted successfully!', type: 'success' } })); } break; case 'share': this.dispatchEvent(new CustomEvent('share-request', { detail: { fileId: id } })); break; case 'star-file': await api.starFile(id); document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: 'File starred successfully!', type: 'success' } })); await this.loadContents(this.currentFolderId); break; case 'unstar-file': await api.unstarFile(id); document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: 'File unstarred successfully!', type: 'success' } })); await this.loadContents(this.currentFolderId); break; case 'star-folder': await api.starFolder(id); document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: 'Folder starred successfully!', type: 'success' } })); await this.loadContents(this.currentFolderId); break; case 'unstar-folder': await api.unstarFolder(id); document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: 'Folder unstarred successfully!', type: 'success' } })); await this.loadContents(this.currentFolderId); break; } } catch (error) { document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: 'Action failed: ' + error.message, type: 'error' } })); } } } customElements.define('file-list', FileList);