MovieMapper/utils/renderer/EpisodeManager.js
Jarian Cottingham be887c3855 Refactor renderer.js into modular class-based structure
- Split large renderer.js file into 8 separate class files:
  - AppState.js: Manages application state
  - FileListManager.js: Handles file list display and manipulation
  - TagManager.js: Manages file tagging functionality
  - EpisodeManager.js: Handles episode number editing and highlighting
  - SearchManager.js: Manages TVDB search and show selection
  - FileManager.js: Handles file operations
  - ModalManager.js: Manages video preview modal
  - ProgressManager.js: Manages progress display
  - UIManager.js: Main coordinator for UI functionality
- Follows SOLID principles and single responsibility
- Improves code maintainability and testability
- All syntax verified with node --check
2026-02-25 02:32:08 -06:00

285 lines
8.7 KiB
JavaScript

/**
* 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;