/** * 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'; const icon = document.createElement('div'); icon.style.cssText = 'font-size: 18px; margin-right: 10px;'; icon.textContent = '\uD83D\uDCC1'; const nameEl = document.createElement('div'); nameEl.className = 'file-name folder-name'; nameEl.textContent = file.name; const durationEl = document.createElement('div'); durationEl.className = 'file-duration'; const qualityEl = document.createElement('div'); qualityEl.className = 'file-quality'; const fpsEl = document.createElement('div'); fpsEl.className = 'file-fps'; const tagsEl = document.createElement('div'); tagsEl.className = 'file-tags'; fileItem.append(icon, nameEl, durationEl, qualityEl, fpsEl, tagsEl); const filePath = file.path; const fileName = file.name; fileItem.addEventListener('click', (e) => { if (e.target !== fileItem && e.target.className !== 'file-name folder-name') { return; } if (this.onFolderClick) { this.onFolderClick(filePath, fileName); } }); 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; const duration = file.duration || '00:00'; const quality = file.quality || 'unknown'; const fps = file.fps || 'unknown'; const dragHandle = document.createElement('div'); dragHandle.className = 'drag-handle'; dragHandle.textContent = '\u22EE\u22EE'; const episodeContainer = document.createElement('div'); episodeContainer.className = 'episode-number-container'; const leftArrow = document.createElement('button'); leftArrow.className = 'episode-arrow episode-arrow-left'; leftArrow.title = 'Decrease episode range'; leftArrow.textContent = '\u25C0'; const episodeNum = document.createElement('div'); episodeNum.className = 'episode-number'; episodeNum.dataset.episodeStart = episodeNumber; episodeNum.dataset.episodeEnd = episodeNumber; episodeNum.textContent = episodeNumber; const rightArrow = document.createElement('button'); rightArrow.className = 'episode-arrow episode-arrow-right'; rightArrow.title = 'Increase episode range'; rightArrow.textContent = '\u25B6'; episodeContainer.append(leftArrow, episodeNum, rightArrow); const nameEl = document.createElement('div'); nameEl.className = 'file-name'; nameEl.textContent = file.name; if (file.isProblematic) { const warn = document.createElement('span'); warn.className = 'problematic-label'; warn.textContent = '\u26A0\ufe0f'; nameEl.prepend(warn, ' '); } const durEl = document.createElement('div'); durEl.className = 'file-duration'; durEl.textContent = duration; const qualEl = document.createElement('div'); qualEl.className = 'file-quality'; qualEl.textContent = quality; const fpsEl = document.createElement('div'); fpsEl.className = 'file-fps'; fpsEl.textContent = fps; const tagsEl = document.createElement('div'); tagsEl.className = 'file-tags'; const extraTag = document.createElement('span'); extraTag.className = 'tag-icon extra-tag'; extraTag.title = 'Mark as Extra'; extraTag.textContent = '\uD83C\uDFF7\uFE0F'; const btsTag = document.createElement('span'); btsTag.className = 'tag-icon behind-the-scenes-tag'; btsTag.title = 'Add Behind the Scenes'; btsTag.textContent = '\uD83C\uDFA5'; const delTag = document.createElement('span'); delTag.className = 'tag-icon delete-tag'; delTag.title = 'Mark for Deletion'; delTag.textContent = '\uD83D\uDDD1\uFE0F'; const previewBtn = document.createElement('span'); previewBtn.className = 'video-preview-btn'; previewBtn.title = 'Preview Video'; previewBtn.textContent = '\uD83C\uDFAC'; const playBtn = document.createElement('button'); playBtn.className = 'play-button'; playBtn.style.cssText = 'opacity: 0.3; cursor: default; flex-shrink: 0;'; playBtn.disabled = true; playBtn.textContent = '\u25B6\uFE0F'; tagsEl.append(extraTag, btsTag, delTag, previewBtn, playBtn); fileItem.append(dragHandle, episodeContainer, nameEl, durEl, qualEl, fpsEl, tagsEl); 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;