/** * FileListManager - Handles file list display and manipulation */ class FileListManager { constructor(fileListEl, onFolderClick = null) { this.fileListEl = fileListEl; this.draggedItem = null; this.onFolderClick = onFolderClick || null; this.hoveredEpisodeRange = null; this._setupHoverHandlers(); } /** * Set callback for folder click events * @param {Function} callback - Function to call when folder is clicked */ setFolderClickCallback(callback) { this.onFolderClick = callback; } /** * Display files in the UI * @param {Array} files - Array of file objects */ displayFiles(files) { // Re-setup hover handlers when displaying files this._setupHoverHandlers(); this.fileListEl.innerHTML = ''; if (files.length === 0) { this.fileListEl.innerHTML = '

No media files found in this directory.

'; return; } // Separate folders from media files const folders = files.filter(f => f.isFolder); const mediaFiles = files.filter(f => !f.isFolder); // Track episode number for media files only let episodeNumber = 1; // Display folders first (not draggable, no episode number) folders.forEach(file => { this._createFolderElement(file); }); // Display media files with episode numbers and drag/drop mediaFiles.forEach((file, index) => { this._createMediaFileElement(file, episodeNumber, index); episodeNumber++; }); } /** * Create folder element * @param {Object} file - File object * @private */ _createFolderElement(file) { const fileItem = document.createElement('div'); fileItem.className = 'file-item folder-item'; fileItem.innerHTML = `
📁
${file.name}
`; // Add click handler to navigate into folder fileItem.addEventListener('click', (e) => { // Don't trigger if clicking on any child elements if (e.target !== fileItem && e.target.className !== 'file-name folder-name') { return; } if (this.onFolderClick) { this.onFolderClick(file.path, file.name); } }); this.fileListEl.appendChild(fileItem); } /** * Create media file element * @param {Object} file - File object * @param {number} episodeNumber - Episode number * @param {number} index - File index */ _createMediaFileElement(file, episodeNumber, index) { const fileItem = document.createElement('div'); fileItem.className = 'file-item'; fileItem.draggable = true; fileItem.dataset.index = index; fileItem.dataset.filePath = file.path; // Use actual duration from file metadata const duration = file.duration || '00:00'; const quality = file.quality || 'unknown'; const fps = file.fps || 'unknown'; // Add label for problematic files const problemLabel = file.isProblematic ? '⚠️ ' : ''; fileItem.innerHTML = `
⋮⋮
${episodeNumber}
${file.name}
${duration}
${quality}
${fps}
🏷️ 🎥 🗑️ 🎬
`; // Add drag and drop event listeners fileItem.addEventListener('dragstart', this._handleDragStart.bind(this)); fileItem.addEventListener('dragend', this._handleDragEnd.bind(this)); fileItem.addEventListener('dragover', this._handleDragOver.bind(this)); fileItem.addEventListener('dragleave', this._handleDragLeave.bind(this)); fileItem.addEventListener('drop', this._handleDrop.bind(this)); this.fileListEl.appendChild(fileItem); } /** * Handle drag start * @param {Event} e - Drag event * @private */ _handleDragStart(e) { const fileItem = this; this.draggedItem = fileItem; fileItem.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', fileItem.dataset.index); } /** * Handle drag end * @param {Event} e - Drag event * @private */ _handleDragEnd(e) { const fileItem = this; fileItem.classList.remove('dragging'); // Remove drag-over class from all items document.querySelectorAll('.file-item').forEach(item => { item.classList.remove('drag-over'); }); this.draggedItem = null; } /** * Handle drag over * @param {Event} e - Drag event * @private */ _handleDragOver(e) { const fileItem = this; e.preventDefault(); e.dataTransfer.dropEffect = 'move'; // Only show drag-over for non-folder items if (!fileItem.classList.contains('folder-item') && fileItem !== this.draggedItem) { fileItem.classList.add('drag-over'); } } /** * Handle drag leave * @param {Event} e - Drag event * @private */ _handleDragLeave(e) { const fileItem = this; fileItem.classList.remove('drag-over'); } /** * Handle drop * @param {Event} e - Drop event */ _handleDrop(e) { const fileItem = this; e.preventDefault(); fileItem.classList.remove('drag-over'); if (fileItem === this.draggedItem || fileItem.classList.contains('folder-item')) return; // Get all media file items (not folders) const fileItems = Array.from(this.fileListEl.querySelectorAll('.file-item:not(.folder-item)')); const draggedIndex = fileItems.indexOf(this.draggedItem); const dropIndex = fileItems.indexOf(fileItem); if (draggedIndex === -1 || dropIndex === -1) return; // Move the dragged item in the DOM if (draggedIndex < dropIndex) { fileItem.parentNode.insertBefore(this.draggedItem, fileItem.nextSibling); } else { fileItem.parentNode.insertBefore(this.draggedItem, fileItem); } // Update episode numbers this.updateEpisodeNumbers(); } /** * Update episode numbers */ updateEpisodeNumbers() { const fileItems = this.fileListEl.querySelectorAll('.file-item:not(.folder-item)'); let episodeNum = 1; fileItems.forEach((item, index) => { const episodeEl = item.querySelector('.episode-number'); if (episodeEl) { const storedStart = episodeEl.dataset.episodeStart; const storedEnd = episodeEl.dataset.episodeEnd; const start = storedStart ? parseInt(storedStart) : episodeNum; const end = storedEnd ? parseInt(storedEnd) : start; const rangeText = start === end ? `${start}` : `${start}-${end}`; episodeEl.textContent = rangeText; episodeEl.dataset.episode = episodeNum; episodeNum = end + 1; } }); } /** * Clear the file list */ clear() { this.fileListEl.innerHTML = ''; } /** * Get all media file items * @returns {NodeList} Media file items */ getMediaFileItems() { return this.fileListEl.querySelectorAll('.file-item:not(.folder-item)'); } /** * Get folder items * @returns {NodeList} Folder items */ getFolderItems() { return this.fileListEl.querySelectorAll('.folder-item'); } /** * Set up hover handlers for file items * @private */ _setupHoverHandlers() { // Use event delegation on the file list this.fileListEl.addEventListener('mouseenter', (e) => { const fileItem = e.target.closest('.file-item'); if (!fileItem || fileItem.classList.contains('folder-item')) return; const episodeEl = fileItem.querySelector('.episode-number'); if (!episodeEl) return; const startEpisode = parseInt(episodeEl.dataset.episodeStart) || 1; const endEpisode = parseInt(episodeEl.dataset.episodeEnd) || startEpisode; // Store hovered range for later use this.hoveredEpisodeRange = { start: startEpisode, end: endEpisode }; // Notify UIManager to highlight episodes if (this.onHoverStart && typeof this.onHoverStart === 'function') { this.onHoverStart(startEpisode, endEpisode, fileItem); } }); this.fileListEl.addEventListener('mouseleave', (e) => { const fileItem = e.target.closest('.file-item'); if (!fileItem) return; // Remove from current file item fileItem.classList.remove('hovered-file'); // Notify UIManager to remove highlights if (this.onHoverEnd && typeof this.onHoverEnd === 'function') { this.onHoverEnd(fileItem); } }); } /** * Highlight files matching episode range * @param {number} startEpisode - Start episode number * @param {number} endEpisode - End episode number */ highlightFilesMatchingEpisode(startEpisode, endEpisode) { const mediaFiles = this.getMediaFileItems(); mediaFiles.forEach(fileItem => { const episodeEl = fileItem.querySelector('.episode-number'); if (!episodeEl) return; const fileStart = parseInt(episodeEl.dataset.episodeStart) || 1; const fileEnd = parseInt(episodeEl.dataset.episodeEnd) || fileStart; // Check if the file's episode range overlaps with the hovered range const overlaps = !(endEpisode < fileStart || startEpisode > fileEnd); if (overlaps) { fileItem.classList.add('hovered-file'); } else { fileItem.classList.remove('hovered-file'); } }); } /** * Remove file highlights */ removeFileHighlights() { const mediaFiles = this.getMediaFileItems(); mediaFiles.forEach(fileItem => { fileItem.classList.remove('hovered-file'); }); } } module.exports = FileListManager;