/** * EpisodeManager - Manages episode number editing and highlighting */ class EpisodeManager { constructor() { this.isUpdatingEpisodeNumbers = false; } /** * Update episode numbers in the file list * @param {HTMLElement} fileListEl - File list element */ updateEpisodeNumbers(fileListEl) { if (this.isUpdatingEpisodeNumbers) { console.log('[updateEpisodeNumbers] Already updating, skipping re-entrant call'); return; } this.isUpdatingEpisodeNumbers = true; try { const fileItems = 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; } }); } finally { this.isUpdatingEpisodeNumbers = false; } } /** * Make episode range editable * @param {HTMLElement} element - Episode number element */ makeEpisodeRangeEditable(element) { if (element.contentEditable === 'true') return; const originalText = element.textContent; const originalStart = element.dataset.episodeStart; const originalEnd = element.dataset.episodeEnd; element.contentEditable = 'true'; element.focus(); element.classList.add('editing'); const range = document.createRange(); range.selectNodeContents(element); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); const saveEdit = () => { const newText = element.textContent.trim(); if (newText !== originalText) { let start, end; if (newText.includes('-')) { const parts = newText.split('-').map(p => parseInt(p.trim())); start = parts[0]; end = parts[1] || start; } else { const num = parseInt(newText); start = num; end = num; } if (!isNaN(start) && !isNaN(end)) { element.dataset.episodeStart = start; element.dataset.episodeEnd = end; console.log(`Episode range updated: ${start}-${end}`); this.updateEpisodeNumbers(element.closest('#file-list')); } else { element.textContent = originalText; element.dataset.episodeStart = originalStart; element.dataset.episodeEnd = originalEnd; } } element.contentEditable = 'false'; element.classList.remove('editing'); }; element.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); saveEdit(); } }, { once: true }); element.addEventListener('blur', saveEdit, { once: true }); } /** * Handle episode arrow click * @param {HTMLElement} element - Arrow element * @param {string} direction - Direction (left or right) * @param {HTMLElement} fileListEl - File list element */ handleEpisodeArrowClick(element, direction, fileListEl) { const fileItem = element.closest('.file-item'); const episodeEl = fileItem.querySelector('.episode-number'); if (!episodeEl) return; // Get current values before updating let start = parseInt(episodeEl.dataset.episodeStart) || 1; let end = parseInt(episodeEl.dataset.episodeEnd) || start; const oldStart = start; const oldEnd = end; // Update the episode range if (direction === 'left') { // Decrease range - move start back by 1 if (start > 1) { start--; } } else { // Increase range - move end forward by 1 end++; } // Update this episode's data attributes episodeEl.dataset.episodeStart = start; episodeEl.dataset.episodeEnd = end; // Get all media file items (not folders) in order const allFileItems = Array.from(fileListEl.querySelectorAll('.file-item:not(.folder-item)')); // Find current index by matching file path const filePath = fileItem.dataset.filePath; let currentIndex = -1; for (let i = 0; i < allFileItems.length; i++) { if (allFileItems[i].dataset.filePath === filePath) { currentIndex = i; break; } } if (currentIndex === -1) return; // For right arrow: shift all subsequent episodes forward by 1 if (direction === 'right') { for (let i = currentIndex + 1; i < allFileItems.length; i++) { const nextItem = allFileItems[i]; const nextEpisodeEl = nextItem.querySelector('.episode-number'); if (nextEpisodeEl) { let nextStart = parseInt(nextEpisodeEl.dataset.episodeStart) || 1; const nextEnd = parseInt(nextEpisodeEl.dataset.episodeEnd) || nextStart; nextEpisodeEl.dataset.episodeStart = nextStart + 1; nextEpisodeEl.dataset.episodeEnd = nextEnd + 1; } } } else { // For left arrow: all subsequent episodes shift down const shiftAmount = oldStart - start; for (let i = currentIndex + 1; i < allFileItems.length; i++) { const nextItem = allFileItems[i]; const nextEpisodeEl = nextItem.querySelector('.episode-number'); if (nextEpisodeEl) { let nextStart = parseInt(nextEpisodeEl.dataset.episodeStart) || 1; const nextEnd = parseInt(nextEpisodeEl.dataset.episodeEnd) || nextStart; nextEpisodeEl.dataset.episodeStart = nextStart - shiftAmount; nextEpisodeEl.dataset.episodeEnd = nextEnd - shiftAmount; } } } // Force immediate DOM update this.updateEpisodeNumbers(fileListEl); } /** * Highlight episodes in the episodes container * @param {number} startEpisode - Start episode number * @param {number} endEpisode - End episode number * @param {HTMLElement} episodesContainer - Episodes container element */ highlightEpisodes(startEpisode, endEpisode, episodesContainer) { if (!episodesContainer) return; const episodeItems = episodesContainer.querySelectorAll('.episode-item'); episodeItems.forEach(item => { const episodeText = item.textContent.trim(); const episodeMatch = episodeText.match(/E(\d+)/); if (episodeMatch) { const episodeNum = parseInt(episodeMatch[1]); if (episodeNum >= startEpisode && episodeNum <= endEpisode) { item.classList.add('highlighted-episode'); } } }); } /** * Remove highlights from all episodes * @param {HTMLElement} episodesContainer - Episodes container element */ removeEpisodeHighlights(episodesContainer) { if (!episodesContainer) return; const episodeItems = episodesContainer.querySelectorAll('.episode-item'); episodeItems.forEach(item => { item.classList.remove('highlighted-episode'); }); } /** * Get episode range from element * @param {HTMLElement} episodeEl - Episode element * @returns {Object} Episode range object */ getEpisodeRange(episodeEl) { const episodeStart = parseInt(episodeEl.dataset.episodeStart) || 1; const episodeEnd = parseInt(episodeEl.dataset.episodeEnd) || episodeStart; return { start: episodeStart, end: episodeEnd }; } /** * Calculate total episode count from file items * @param {HTMLElement} fileListEl - File list element * @returns {number} Total episode count */ calculateTotalEpisodeCount(fileListEl) { const mediaFiles = fileListEl.querySelectorAll('.file-item:not(.folder-item)'); let totalEpisodeCount = 0; mediaFiles.forEach(item => { const episodeEl = item.querySelector('.episode-number'); if (episodeEl) { const { start, end } = this.getEpisodeRange(episodeEl); totalEpisodeCount += (end - start + 1); } }); return totalEpisodeCount; } /** * Get last episode end from file items * @param {HTMLElement} fileListEl - File list element * @returns {number} Last episode end number */ getLastEpisodeEnd(fileListEl) { const mediaFiles = fileListEl.querySelectorAll('.file-item:not(.folder-item)'); let lastEpisodeEnd = 0; mediaFiles.forEach((item, index) => { const episodeEl = item.querySelector('.episode-number'); if (episodeEl) { const episodeEnd = parseInt(episodeEl.dataset.episodeEnd) || 1; if (index === 0 || episodeEnd > lastEpisodeEnd) { lastEpisodeEnd = episodeEnd; } } }); return lastEpisodeEnd; } } module.exports = EpisodeManager;