...
This commit is contained in:
		
							
								
								
									
										290
									
								
								static/js/file-tree.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										290
									
								
								static/js/file-tree.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,290 @@ | ||||
| /** | ||||
|  * File Tree Component | ||||
|  * Manages the hierarchical file tree display and interactions | ||||
|  */ | ||||
|  | ||||
| class FileTree { | ||||
|     constructor(containerId, webdavClient) { | ||||
|         this.container = document.getElementById(containerId); | ||||
|         this.webdavClient = webdavClient; | ||||
|         this.tree = []; | ||||
|         this.selectedPath = null; | ||||
|         this.onFileSelect = null; | ||||
|         this.onFolderSelect = null; | ||||
|          | ||||
|         this.setupEventListeners(); | ||||
|     } | ||||
|      | ||||
|     setupEventListeners() { | ||||
|         // Click handler for tree nodes | ||||
|         this.container.addEventListener('click', (e) => { | ||||
|             const node = e.target.closest('.tree-node'); | ||||
|             if (!node) return; | ||||
|              | ||||
|             const path = node.dataset.path; | ||||
|             const isDir = node.dataset.isdir === 'true'; | ||||
|              | ||||
|             // Toggle folder | ||||
|             if (e.target.closest('.tree-toggle')) { | ||||
|                 this.toggleFolder(node); | ||||
|                 return; | ||||
|             } | ||||
|              | ||||
|             // Select node | ||||
|             if (isDir) { | ||||
|                 this.selectFolder(path); | ||||
|             } else { | ||||
|                 this.selectFile(path); | ||||
|             } | ||||
|         }); | ||||
|          | ||||
|         // Context menu | ||||
|         this.container.addEventListener('contextmenu', (e) => { | ||||
|             const node = e.target.closest('.tree-node'); | ||||
|             if (!node) return; | ||||
|              | ||||
|             e.preventDefault(); | ||||
|             const path = node.dataset.path; | ||||
|             const isDir = node.dataset.isdir === 'true'; | ||||
|              | ||||
|             window.showContextMenu(e.clientX, e.clientY, { path, isDir }); | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     async load() { | ||||
|         try { | ||||
|             const items = await this.webdavClient.propfind('', 'infinity'); | ||||
|             this.tree = this.webdavClient.buildTree(items); | ||||
|             this.render(); | ||||
|         } catch (error) { | ||||
|             console.error('Failed to load file tree:', error); | ||||
|             showNotification('Failed to load files', 'error'); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     render() { | ||||
|         this.container.innerHTML = ''; | ||||
|         this.renderNodes(this.tree, this.container, 0); | ||||
|     } | ||||
|      | ||||
|     renderNodes(nodes, parentElement, level) { | ||||
|         nodes.forEach(node => { | ||||
|             const nodeElement = this.createNodeElement(node, level); | ||||
|             parentElement.appendChild(nodeElement); | ||||
|              | ||||
|             if (node.children && node.children.length > 0) { | ||||
|                 const childContainer = document.createElement('div'); | ||||
|                 childContainer.className = 'tree-children'; | ||||
|                 childContainer.style.display = 'none'; | ||||
|                 nodeElement.appendChild(childContainer); | ||||
|                 this.renderNodes(node.children, childContainer, level + 1); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     createNodeElement(node, level) { | ||||
|         const div = document.createElement('div'); | ||||
|         div.className = 'tree-node'; | ||||
|         div.dataset.path = node.path; | ||||
|         div.dataset.isdir = node.isDirectory; | ||||
|         div.style.paddingLeft = `${level * 20 + 10}px`; | ||||
|          | ||||
|         // Toggle arrow for folders | ||||
|         if (node.isDirectory) { | ||||
|             const toggle = document.createElement('span'); | ||||
|             toggle.className = 'tree-toggle'; | ||||
|             toggle.innerHTML = '<i class="bi bi-chevron-right"></i>'; | ||||
|             div.appendChild(toggle); | ||||
|         } else { | ||||
|             const spacer = document.createElement('span'); | ||||
|             spacer.className = 'tree-spacer'; | ||||
|             spacer.style.width = '16px'; | ||||
|             spacer.style.display = 'inline-block'; | ||||
|             div.appendChild(spacer); | ||||
|         } | ||||
|          | ||||
|         // Icon | ||||
|         const icon = document.createElement('i'); | ||||
|         if (node.isDirectory) { | ||||
|             icon.className = 'bi bi-folder-fill'; | ||||
|             icon.style.color = '#dcb67a'; | ||||
|         } else { | ||||
|             icon.className = 'bi bi-file-earmark-text'; | ||||
|             icon.style.color = '#6a9fb5'; | ||||
|         } | ||||
|         div.appendChild(icon); | ||||
|          | ||||
|         // Name | ||||
|         const name = document.createElement('span'); | ||||
|         name.className = 'tree-name'; | ||||
|         name.textContent = node.name; | ||||
|         div.appendChild(name); | ||||
|          | ||||
|         // Size for files | ||||
|         if (!node.isDirectory && node.size) { | ||||
|             const size = document.createElement('span'); | ||||
|             size.className = 'tree-size'; | ||||
|             size.textContent = this.formatSize(node.size); | ||||
|             div.appendChild(size); | ||||
|         } | ||||
|          | ||||
|         return div; | ||||
|     } | ||||
|      | ||||
|     toggleFolder(nodeElement) { | ||||
|         const childContainer = nodeElement.querySelector('.tree-children'); | ||||
|         if (!childContainer) return; | ||||
|          | ||||
|         const toggle = nodeElement.querySelector('.tree-toggle i'); | ||||
|         const isExpanded = childContainer.style.display !== 'none'; | ||||
|          | ||||
|         if (isExpanded) { | ||||
|             childContainer.style.display = 'none'; | ||||
|             toggle.className = 'bi bi-chevron-right'; | ||||
|         } else { | ||||
|             childContainer.style.display = 'block'; | ||||
|             toggle.className = 'bi bi-chevron-down'; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     selectFile(path) { | ||||
|         this.selectedPath = path; | ||||
|         this.updateSelection(); | ||||
|         if (this.onFileSelect) { | ||||
|             this.onFileSelect({ path, isDirectory: false }); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     selectFolder(path) { | ||||
|         this.selectedPath = path; | ||||
|         this.updateSelection(); | ||||
|         if (this.onFolderSelect) { | ||||
|             this.onFolderSelect({ path, isDirectory: true }); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     updateSelection() { | ||||
|         // Remove previous selection | ||||
|         this.container.querySelectorAll('.tree-node').forEach(node => { | ||||
|             node.classList.remove('selected'); | ||||
|         }); | ||||
|          | ||||
|         // Add selection to current | ||||
|         if (this.selectedPath) { | ||||
|             const node = this.container.querySelector(`[data-path="${this.selectedPath}"]`); | ||||
|             if (node) { | ||||
|                 node.classList.add('selected'); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     formatSize(bytes) { | ||||
|         if (bytes === 0) return '0 B'; | ||||
|         const k = 1024; | ||||
|         const sizes = ['B', 'KB', 'MB', 'GB']; | ||||
|         const i = Math.floor(Math.log(bytes) / Math.log(k)); | ||||
|         return Math.round(bytes / Math.pow(k, i) * 10) / 10 + ' ' + sizes[i]; | ||||
|     } | ||||
|      | ||||
|     async createFile(parentPath, filename) { | ||||
|         try { | ||||
|             const fullPath = parentPath ? `${parentPath}/${filename}` : filename; | ||||
|             await this.webdavClient.put(fullPath, '# New File\n\nStart typing...\n'); | ||||
|             await this.load(); | ||||
|             showNotification('File created', 'success'); | ||||
|             return fullPath; | ||||
|         } catch (error) { | ||||
|             console.error('Failed to create file:', error); | ||||
|             showNotification('Failed to create file', 'error'); | ||||
|             throw error; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     async createFolder(parentPath, foldername) { | ||||
|         try { | ||||
|             const fullPath = parentPath ? `${parentPath}/${foldername}` : foldername; | ||||
|             await this.webdavClient.mkcol(fullPath); | ||||
|             await this.load(); | ||||
|             showNotification('Folder created', 'success'); | ||||
|             return fullPath; | ||||
|         } catch (error) { | ||||
|             console.error('Failed to create folder:', error); | ||||
|             showNotification('Failed to create folder', 'error'); | ||||
|             throw error; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     async uploadFile(parentPath, file) { | ||||
|         try { | ||||
|             const fullPath = parentPath ? `${parentPath}/${file.name}` : file.name; | ||||
|             const content = await file.arrayBuffer(); | ||||
|             await this.webdavClient.putBinary(fullPath, content); | ||||
|             await this.load(); | ||||
|             showNotification(`Uploaded ${file.name}`, 'success'); | ||||
|             return fullPath; | ||||
|         } catch (error) { | ||||
|             console.error('Failed to upload file:', error); | ||||
|             showNotification('Failed to upload file', 'error'); | ||||
|             throw error; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     async downloadFile(path) { | ||||
|         try { | ||||
|             const content = await this.webdavClient.get(path); | ||||
|             const filename = path.split('/').pop(); | ||||
|             this.triggerDownload(content, filename); | ||||
|             showNotification('Downloaded', 'success'); | ||||
|         } catch (error) { | ||||
|             console.error('Failed to download file:', error); | ||||
|             showNotification('Failed to download file', 'error'); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     async downloadFolder(path) { | ||||
|         try { | ||||
|             showNotification('Creating zip...', 'info'); | ||||
|             // Get all files in folder | ||||
|             const items = await this.webdavClient.propfind(path, 'infinity'); | ||||
|             const files = items.filter(item => !item.isDirectory); | ||||
|              | ||||
|             // Use JSZip to create zip file | ||||
|             const JSZip = window.JSZip; | ||||
|             if (!JSZip) { | ||||
|                 throw new Error('JSZip not loaded'); | ||||
|             } | ||||
|              | ||||
|             const zip = new JSZip(); | ||||
|             const folder = zip.folder(path.split('/').pop() || 'download'); | ||||
|              | ||||
|             // Add all files to zip | ||||
|             for (const file of files) { | ||||
|                 const content = await this.webdavClient.get(file.path); | ||||
|                 const relativePath = file.path.replace(path + '/', ''); | ||||
|                 folder.file(relativePath, content); | ||||
|             } | ||||
|              | ||||
|             // Generate zip | ||||
|             const zipBlob = await zip.generateAsync({ type: 'blob' }); | ||||
|             const zipFilename = `${path.split('/').pop() || 'download'}.zip`; | ||||
|             this.triggerDownload(zipBlob, zipFilename); | ||||
|             showNotification('Downloaded', 'success'); | ||||
|         } catch (error) { | ||||
|             console.error('Failed to download folder:', error); | ||||
|             showNotification('Failed to download folder', 'error'); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     triggerDownload(content, filename) { | ||||
|         const blob = content instanceof Blob ? content : new Blob([content]); | ||||
|         const url = URL.createObjectURL(blob); | ||||
|         const a = document.createElement('a'); | ||||
|         a.href = url; | ||||
|         a.download = filename; | ||||
|         document.body.appendChild(a); | ||||
|         a.click(); | ||||
|         document.body.removeChild(a); | ||||
|         URL.revokeObjectURL(url); | ||||
|     } | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user