- `;
-
- // Add drag and drop event listeners
- fileItem.addEventListener('dragstart', handleDragStart);
- fileItem.addEventListener('dragend', handleDragEnd);
- fileItem.addEventListener('dragover', handleDragOver);
- fileItem.addEventListener('dragleave', handleDragLeave);
- fileItem.addEventListener('drop', handleDrop);
-
- // Add click event to make file name editable
- const fileNameElement = fileItem.querySelector('.file-name');
- fileNameElement.addEventListener('click', function(e) {
- e.stopPropagation();
- makeEditable(e.target);
- });
-
- // Add click handlers for episode arrow buttons
- const arrowLeft = fileItem.querySelector('.episode-arrow-left');
- const arrowRight = fileItem.querySelector('.episode-arrow-right');
-
- if (arrowLeft) {
- arrowLeft.addEventListener('click', function(e) {
- e.stopPropagation();
- handleEpisodeArrowClick(this, 'left');
- });
- }
-
- if (arrowRight) {
- arrowRight.addEventListener('click', function(e) {
- e.stopPropagation();
- handleEpisodeArrowClick(this, 'right');
- });
- }
-
- // Add hover effects for tags
- const tagIcons = fileItem.querySelectorAll('.tag-icon');
- tagIcons.forEach(icon => {
- icon.addEventListener('mouseenter', function() {
- this.style.opacity = '1';
- this.style.transform = 'scale(1.1)';
- });
-
- icon.addEventListener('mouseleave', function() {
- this.style.opacity = '0.7';
- this.style.transform = 'scale(1)';
- });
-
- // Add click handlers for tagging
- icon.addEventListener('click', function(e) {
- e.stopPropagation(); // Prevent event bubbling
- const filePath = this.dataset.filePath;
- let tagType;
- if (this.classList.contains('extra-tag')) {
- tagType = 'extra';
- } else if (this.classList.contains('commentary-tag')) {
- tagType = 'commentary';
- } else if (this.classList.contains('delete-tag')) {
- tagType = 'delete';
- } else {
- return;
- }
-
- // Check if file is already tagged with this type
- const fileItem = this.closest('.file-item');
- const isTagged = fileItem.hasAttribute('data-tagged-' + tagType);
-
- if (isTagged) {
- // Untag the file
- untagFile(filePath, tagType);
- } else {
- // If tagging this type, untag any existing tag of other types
- ['extra', 'commentary', 'delete'].forEach(otherType => {
- if (otherType !== tagType && fileItem.hasAttribute('data-tagged-' + otherType)) {
- untagFile(filePath, otherType);
- }
- });
- // Tag the file
- addTagToEpisode(filePath, tagType);
- }
- });
- });
-
- // Add click handler for preview button
- const previewBtn = fileItem.querySelector('.video-preview-btn');
- previewBtn.addEventListener('click', function(e) {
- e.stopPropagation(); // Prevent event bubbling
- const filePath = this.dataset.filePath;
- openVideoPreview(filePath);
- });
-
- // Add click handler for play button
- const playButton = fileItem.querySelector('.play-button');
- playButton.addEventListener('click', async function(e) {
- e.stopPropagation();
- console.log('Play button clicked for file:', file.path);
-
- // Get the file item and tag type
- const fileItemEl = this.closest('.file-item');
- let tagType = null;
- if (fileItemEl.hasAttribute('data-tagged-extra')) {
- tagType = 'extra';
- } else if (fileItemEl.hasAttribute('data-tagged-commentary')) {
- tagType = 'commentary';
- } else if (fileItemEl.hasAttribute('data-tagged-delete')) {
- tagType = 'delete';
- }
-
- console.log('Tag type determined:', tagType);
- if (tagType) {
- // Move the file to the appropriate folder
- const result = await moveTaggedFile(file.path, tagType);
- if (result.success) {
- // Remove the item from the file list after successful move
- fileItemEl.remove();
- updateTaggedCount();
- updateEpisodeNumbers();
- checkEpisodeCountMatch();
- }
- } else {
- console.log('No tag type found for file');
- }
- });
-
- fileListEl.appendChild(fileItem);
- episodeNumber++;
- });
-
- // Check if episode count matches after displaying files
- checkEpisodeCountMatch();
-}
-
-// Drag and drop handlers
-let draggedItem = null;
-
-function handleDragStart(e) {
- draggedItem = this;
- this.classList.add('dragging');
- e.dataTransfer.effectAllowed = 'move';
- e.dataTransfer.setData('text/plain', this.dataset.index);
-}
-
-function handleDragEnd(e) {
- this.classList.remove('dragging');
- // Remove drag-over class from all items
- document.querySelectorAll('.file-item').forEach(item => {
- item.classList.remove('drag-over');
- });
- draggedItem = null;
-}
-
-function handleDragOver(e) {
- e.preventDefault();
- e.dataTransfer.dropEffect = 'move';
-
- // Only show drag-over for non-folder items
- if (!this.classList.contains('folder-item') && this !== draggedItem) {
- this.classList.add('drag-over');
- }
-}
-
-function handleDragLeave(e) {
- this.classList.remove('drag-over');
-}
-
-function handleDrop(e) {
- e.preventDefault();
- this.classList.remove('drag-over');
-
- if (this === draggedItem || this.classList.contains('folder-item')) return;
-
- // Get all media file items (not folders)
- const fileItems = Array.from(fileListEl.querySelectorAll('.file-item:not(.folder-item)'));
- const draggedIndex = fileItems.indexOf(draggedItem);
- const dropIndex = fileItems.indexOf(this);
-
- if (draggedIndex === -1 || dropIndex === -1) return;
-
- // Move the dragged item in the DOM
- if (draggedIndex < dropIndex) {
- this.parentNode.insertBefore(draggedItem, this.nextSibling);
- } else {
- this.parentNode.insertBefore(draggedItem, this);
- }
-
- // Update episode numbers
- updateEpisodeNumbers();
-}
-
-function updateEpisodeNumbers() {
- if (isUpdatingEpisodeNumbers) {
- console.log('[updateEpisodeNumbers] Already updating, skipping re-entrant call');
- return;
- }
-
- isUpdatingEpisodeNumbers = true;
-
- try {
- const fileItems = fileListEl.querySelectorAll('.file-item:not(.folder-item)');
- let episodeNum = 1;
-
- console.log('[updateEpisodeNumbers] Starting with episodeNum:', episodeNum);
- console.log('[updateEpisodeNumbers] Total items:', fileItems.length);
- console.trace('[updateEpisodeNumbers] Call stack');
-
- fileItems.forEach((item, index) => {
- const episodeEl = item.querySelector('.episode-number');
- if (episodeEl) {
- const storedStart = episodeEl.dataset.episodeStart;
- const storedEnd = episodeEl.dataset.episodeEnd;
- console.log(`[updateEpisodeNumbers] Item ${index}: storedStart="${storedStart}", storedEnd="${storedEnd}"`);
-
- 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;
- console.log(`[updateEpisodeNumbers] Item ${index}: calculated start=${start}, end=${end}, display=${rangeText}, next episodeNum=${end + 1}`);
- episodeNum = end + 1;
- }
- });
- } finally {
- isUpdatingEpisodeNumbers = false;
- }
-}
-
-// Make an episode number editable for range editing
-function 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 = function() {
- 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(`[makeEpisodeRangeEditable] Episode range updated: ${start}-${end}`);
- updateEpisodeNumbers();
- } 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 button clicks
-function handleEpisodeArrowClick(element, direction) {
- 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
- updateEpisodeNumbers();
-}
-
-// Make a file name editable
-function makeEditable(element) {
- // Prevent editing if already in edit mode
- if (element.contentEditable === 'true') return;
-
- // Store original text
- const originalText = element.textContent;
-
- // Make element editable
- element.contentEditable = 'true';
- element.focus();
- element.classList.add('editing');
-
- // Select all text when editing starts
- const range = document.createRange();
- range.selectNodeContents(element);
- const selection = window.getSelection();
- selection.removeAllRanges();
- selection.addRange(range);
-
- // Handle saving when user finishes editing
- const saveEdit = async function() {
- if (element.textContent.trim() !== originalText.trim()) {
- // Send request to main process to rename the file
- try {
- const result = await ipcRenderer.invoke('rename-file', {
- oldPath: element.dataset.filePath,
- newName: element.textContent.trim()
- });
-
- if (result.success) {
- console.log('File renamed successfully:', result.message);
- // Update the file name in the UI to reflect the change
- element.textContent = element.textContent.trim();
-
- // Also update the data attribute to reflect the new path
- const oldPath = element.dataset.filePath;
- const newPath = oldPath.substring(0, oldPath.lastIndexOf(path.sep) + 1) + element.textContent.trim();
- element.dataset.filePath = newPath;
- } else {
- console.error('Failed to rename file:', result.error);
- // Revert to original name on failure
- element.textContent = originalText;
- }
- } catch (error) {
- console.error('Error renaming file:', error);
- // Revert to original name on error
- element.textContent = originalText;
- }
- }
-
- // Clean up
- element.contentEditable = 'false';
- element.classList.remove('editing');
- };
-
- // Save on Enter key or blur
- element.addEventListener('keydown', function(e) {
- if (e.key === 'Enter') {
- e.preventDefault();
- saveEdit();
- }
- });
-
- element.addEventListener('blur', saveEdit);
-}
-
-// Function to handle tagging
-function addTagToEpisode(filePath, tagType) {
- console.log(`Tagging file ${filePath} as ${tagType}`);
-
- // Find the file item in the UI
- const fileItems = document.querySelectorAll('.file-item');
- fileItems.forEach(item => {
- const fileNameElement = item.querySelector('.file-name');
- if (fileNameElement && fileNameElement.dataset.filePath === filePath) {
- // Add visual indication of tagging using data attribute for CSS styling
- const tagIcon = item.querySelector(`.${tagType}-tag`);
- if (tagIcon) {
- // Set color based on tag type
- let tagColor;
- if (tagType === 'extra') {
- tagColor = '#FFD700'; // Yellow
- } else if (tagType === 'commentary') {
- tagColor = '#17a2b8'; // Teal
- } else if (tagType === 'delete') {
- tagColor = '#dc3545'; // Red
- }
-
- // Make the icon fully saturated and highlight
- tagIcon.style.opacity = '1';
- tagIcon.style.filter = 'none';
- tagIcon.style.color = tagColor;
- tagIcon.style.textShadow = `0 0 15px ${tagColor}`;
- tagIcon.style.transform = 'scale(1.3)';
-
- // Add a data attribute to track that this file is tagged
- item.setAttribute('data-tagged-' + tagType, 'true');
-
- // Enable the play button
- const playButton = item.querySelector('.play-button');
- if (playButton) {
- playButton.style.opacity = '1';
- playButton.style.cursor = 'pointer';
- playButton.disabled = false;
- playButton.style.pointerEvents = 'auto';
- }
- }
- }
- });
-
- // Update the tagged count display
- updateTaggedCount();
-
- // In a real implementation, this would save to a database or file
- // For now, we'll just log to console
- console.log(`File ${filePath} tagged as ${tagType} - would be saved in real implementation`);
-}
-
-// Function to handle moving tagged files (also logs audit)
-function moveTaggedFile(filePath, tagType) {
- console.log(`[RENDERER] Moving file ${filePath} to ${tagType} folder`);
-
- // Log audit event
- logAuditEvent('move_file', {
- filePath: filePath,
- folderName: tagType
- });
-
- // Send request to main process to move the file
- return ipcRenderer.invoke('move-file-to-folder', {
- filePath: filePath,
- folderName: tagType
- }).then(result => {
- console.log(`[RENDERER] Move result:`, result);
- if (result.success) {
- console.log(`[RENDERER] File moved successfully to ${tagType} folder`);
- // Show visual feedback that directory was created
- showDirectoryFeedback(tagType);
- return { success: true, filePath };
- } else {
- console.error(`[RENDERER] Failed to move file: ${result.error}`);
- return { success: false, error: result.error, filePath };
- }
- }).catch(error => {
- console.error('[RENDERER] Error moving file:', error);
- return { success: false, error: error.message, filePath };
- });
-}
-
-// Function to move all tagged files at once
-async function moveAllTaggedFiles() {
- const taggedItems = document.querySelectorAll('.file-item[data-tagged-extra], .file-item[data-tagged-commentary], .file-item[data-tagged-delete]');
-
- if (taggedItems.length === 0) {
- console.log('[RENDERER] No tagged files to move');
- return;
- }
-
- console.log(`[RENDERER] Moving ${taggedItems.length} tagged files`);
-
- // Visual feedback - change circle color while processing
- const taggedCircle = document.getElementById('tagged-circle');
- if (taggedCircle) {
- taggedCircle.style.backgroundColor = '#ffc107';
- taggedCircle.style.pointerEvents = 'none';
- }
-
- const results = [];
- for (const item of taggedItems) {
- const filePath = item.querySelector('.file-name').dataset.filePath;
- let tagType;
- if (item.hasAttribute('data-tagged-extra')) {
- tagType = 'extra';
- } else if (item.hasAttribute('data-tagged-commentary')) {
- tagType = 'commentary';
- } else if (item.hasAttribute('data-tagged-delete')) {
- tagType = 'delete';
- }
-
- const result = await moveTaggedFile(filePath, tagType);
- results.push(result);
-
- if (result.success) {
- // Remove the item from the file list after successful move
- item.remove();
- }
- }
-
- // Reset circle appearance
- if (taggedCircle) {
- taggedCircle.style.backgroundColor = '';
- taggedCircle.style.pointerEvents = '';
- }
-
- // Update the tagged count after all moves
- updateTaggedCount();
- updateEpisodeNumbers();
- checkEpisodeCountMatch();
-
- // Summary of results
- const successful = results.filter(r => r.success).length;
- const failed = results.filter(r => !r.success).length;
-
- if (failed > 0) {
- alert(`Moved ${successful} files. ${failed} files failed to move.`);
- } else if (successful > 0) {
- console.log(`[RENDERER] Successfully moved all ${successful} files`);
- }
-}
-
-// Function to begin mapping files to Jellyfin naming convention
-async function beginMapping() {
- const btn = document.getElementById('begin-mapping-btn');
-
- // Get all media file items (not folders)
- const fileItems = document.querySelectorAll('.file-item:not(.folder-item)');
-
- if (fileItems.length === 0) {
- alert('No media files to map. Please select a directory first.');
- return;
- }
-
- if (!currentDirectory) {
- alert('No directory selected.');
- return;
- }
-
- // Disable button during processing
- btn.disabled = true;
- btn.textContent = 'Mapping...';
-
- // Collect file data with episode ranges from UI order
- const filesToMap = [];
- fileItems.forEach((item, index) => {
- const fileNameEl = item.querySelector('.file-name');
- const qualityEl = item.querySelector('.file-quality');
- const episodeNumEl = item.querySelector('.episode-number');
-
- if (fileNameEl && fileNameEl.dataset.filePath) {
- const episodeStart = episodeNumEl ? parseInt(episodeNumEl.dataset.episodeStart || (index + 1)) : (index + 1);
- const episodeEnd = episodeNumEl ? parseInt(episodeNumEl.dataset.episodeEnd || episodeStart) : episodeStart;
-
- filesToMap.push({
- filePath: fileNameEl.dataset.filePath,
- quality: qualityEl ? qualityEl.textContent : '',
- episodeStart: episodeStart,
- episodeEnd: episodeEnd
- });
- }
- });
-
- // Get TVDB ID from current show if selected
- const tvdbId = currentShow ? currentShow.id : null;
-
- try {
- const result = await ipcRenderer.invoke('begin-mapping', {
- directory: currentDirectory,
- files: filesToMap,
- tvdbId: tvdbId
- });
-
- if (result.success) {
- alert('Mapping Complete!');
- // Update currentDirectory if it changed (show folder was renamed)
- const newDir = result.newDirectory || currentDirectory;
- currentDirectory = newDir;
- // Refresh the file list to show new names
- openDirectory(newDir);
- } else {
- alert('Mapping failed: ' + (result.error || 'Unknown error'));
- }
- } catch (error) {
- console.error('Mapping error:', error);
- alert('Mapping failed: ' + error.message);
- } finally {
- btn.disabled = false;
- btn.textContent = 'Begin Mapping';
- }
-}
-
-// Function to display created folders in UI
-function displayCreatedFolder(folderName) {
- // Check if folder entry already exists
- const existingFolder = document.querySelector(`.folder-item[data-folder-name="${folderName}"]`);
- if (existingFolder) {
- // Update the file count
- const countEl = existingFolder.querySelector('.folder-count');
- if (countEl) {
- const currentCount = parseInt(countEl.textContent) || 0;
- countEl.textContent = currentCount + 1;
- }
- return;
- }
-
- // Create folder item element
- const folderItem = document.createElement('div');
- folderItem.className = 'folder-item';
- folderItem.setAttribute('data-folder-name', folderName);
-
- let displayName, folderIcon, folderColor;
- if (folderName === 'extra') {
- displayName = 'extras';
- folderIcon = '📁';
- folderColor = '#FFD700';
- } else if (folderName === 'commentary') {
- displayName = 'commentary';
- folderIcon = '💬';
- folderColor = '#17a2b8';
- } else if (folderName === 'delete') {
- displayName = 'delete';
- folderIcon = '🗑️';
- folderColor = '#dc3545';
- }
-
- folderItem.innerHTML = `
-
${folderIcon}
-
${displayName}
-
1
- `;
-
- folderItem.style.cssText = `
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 12px;
- margin-bottom: 8px;
- background-color: #16213e;
- border: 2px solid ${folderColor};
- border-radius: 8px;
- cursor: pointer;
- `;
-
- // Insert at the top of the file list
- if (fileListEl.firstChild) {
- fileListEl.insertBefore(folderItem, fileListEl.firstChild);
- } else {
- fileListEl.appendChild(folderItem);
- }
-
- console.log(`Folder "${displayName}" displayed in UI`);
-}
-
-// Function to show visual feedback for directory creation
-function showDirectoryFeedback(folderName) {
- console.log(`Directory "${folderName}" created and file moved`);
- // Display the folder in the UI
- displayCreatedFolder(folderName);
-}
-
-// Function to handle untagging
-function untagFile(filePath, tagType) {
- console.log(`Untagging file ${filePath} from ${tagType}`);
-
- // Find the file item in the UI
- const fileItems = document.querySelectorAll('.file-item');
- fileItems.forEach(item => {
- const fileNameElement = item.querySelector('.file-name');
- if (fileNameElement && fileNameElement.dataset.filePath === filePath) {
- // Remove visual indication of tagging
- const tagIcon = item.querySelector(`.${tagType}-tag`);
- if (tagIcon) {
- // Reset to original appearance
- tagIcon.style.opacity = '0.7';
- tagIcon.style.filter = 'none';
- tagIcon.style.color = ''; // Reset to default color
- tagIcon.style.textShadow = 'none';
- tagIcon.style.transform = 'scale(1)';
- tagIcon.style.boxShadow = 'none';
-
- // Remove the data attribute
- item.removeAttribute('data-tagged-' + tagType);
-
- // Disable the play button when untagging (only if no other tags)
- const hasOtherTags = item.hasAttribute('data-tagged-extra') ||
- item.hasAttribute('data-tagged-commentary') ||
- item.hasAttribute('data-tagged-delete');
- if (!hasOtherTags) {
- const playButton = item.querySelector('.play-button');
- if (playButton) {
- playButton.style.opacity = '0.3';
- playButton.style.cursor = 'default';
- playButton.disabled = true;
- playButton.style.pointerEvents = 'none';
- }
- }
- }
- }
- });
-
- // Update the tagged count display
- updateTaggedCount();
-
- // In a real implementation, this would remove from database or file
- // For now, we'll just log to console
- console.log(`File ${filePath} untagged from ${tagType} - would be removed in real implementation`);
-}
-
-// Function to update the tagged items count display
-function updateTaggedCount() {
- const taggedItems = document.querySelectorAll('.file-item[data-tagged-extra], .file-item[data-tagged-commentary], .file-item[data-tagged-delete]');
- const count = taggedItems.length;
- const taggedCircle = document.getElementById('tagged-circle');
- const taggedCount = document.getElementById('tagged-count');
-
- if (taggedCount) {
- taggedCount.textContent = count;
- }
-
- // Animate the circle position based on count
- if (taggedCircle) {
- if (count === 0) {
- // Move down out of sight
- taggedCircle.style.transform = 'translateY(100px)';
- taggedCircle.style.bottom = '-100px';
- } else {
- // Move up to show count (appears at the same spot at bottom right)
- taggedCircle.style.transform = 'translateY(0)';
- taggedCircle.style.bottom = '20px';
- }
- }
-}
-
-// Function to check if media file count matches selected season episode count
-function checkEpisodeCountMatch() {
- const fileList = document.getElementById('file-list');
- // Count only actual media files (not folders)
- const mediaFiles = document.querySelectorAll('.file-item:not(.folder-item)');
-
- // Calculate total episode range (sum of all episode ranges)
- let totalEpisodeCount = 0;
- let lastEpisodeEnd = 0;
-
- mediaFiles.forEach((item, index) => {
- const episodeEl = item.querySelector('.episode-number');
- if (episodeEl) {
- const episodeStart = parseInt(episodeEl.dataset.episodeStart || (index + 1));
- const episodeEnd = parseInt(episodeEl.dataset.episodeEnd || episodeStart);
-
- // Calculate episodes in this range
- const rangeSize = episodeEnd - episodeStart + 1;
- totalEpisodeCount += rangeSize;
-
- // Track the last episode end for sequential checking
- if (index === 0 || episodeEnd > lastEpisodeEnd) {
- lastEpisodeEnd = episodeEnd;
- }
- }
- });
-
- // Use lastEpisodeEnd for comparison (handles ranges properly)
- // For example: if we have episodes 1-3, 4-5, the last episode is 5, not 2 files
- if (selectedSeasonEpisodeCount > 0 && lastEpisodeEnd === selectedSeasonEpisodeCount) {
- // Perfect match - add green outline
- fileList.style.border = '3px solid #28a745';
- fileList.style.boxShadow = '0 0 15px rgba(40, 167, 69, 0.4)';
- } else {
- // No match or no season selected - remove green outline
- fileList.style.border = '';
- fileList.style.boxShadow = '';
- }
-}
-
-// Initialize tagged count on page load
-document.addEventListener('DOMContentLoaded', function() {
- updateTaggedCount();
-});
-
-// Function to log audit events to the directory's .audit file
-function logAuditEvent(action, details) {
- // This function will be called from the renderer process to notify main process
- console.log(`[AUDIT] ${action}:`, details);
- // In a real implementation, this would send an IPC message to main process to log audit
-}
-
-// Search shows function
-async function searchShows() {
- const query = searchInput.value.trim();
-
- if (query.length < 2) {
- searchResultsEl.innerHTML = '';
- return;
- }
-
- try {
- const result = await ipcRenderer.invoke('search-tvdb', query);
-
- if (result.success) {
- displaySearchResults(result.results);
- } else {
- throw new Error(result.error);
- }
- } catch (error) {
- console.error('Error searching shows:', error);
- searchResultsEl.innerHTML = `
Error: ${error.message}
`;
- }
-}
-
-// Clear search results when a show is selected
-function clearSearchResults() {
- searchResultsEl.innerHTML = '';
-}
-
-// Test TVDB API connectivity
-async function testTVDBAPI() {
- try {
- const result = await ipcRenderer.invoke('test-tvdb-api');
- console.log('TVDB API Test:', result);
- if (result.success) {
- console.log('TVDB API is working:', result.message);
- alert('TVDB API Test Successful!\n' + result.message);
- } else {
- console.error('TVDB API test failed:', result.error);
- alert('TVDB API Test Failed!\n' + result.error);
- }
- } catch (error) {
- console.error('Error testing TVDB API:', error);
- alert('Error testing TVDB API: ' + error.message);
- }
-}
-
-// Add a test button to the UI (you can add this to the HTML or call it manually)
-// For now, we'll just make it available in the console
-console.log('TVDB API test function available: testTVDBAPI()');
-
-// Debug function to log problematic file info
-async function debugProblematicFile(filePath) {
- try {
- const result = await ipcRenderer.invoke('log-file-info', filePath);
- if (result.success) {
- console.log('File info:', result.fileInfo);
- } else {
- console.error('Failed to get file info:', result.error);
- }
- } catch (error) {
- console.error('Error debugging file:', error);
- }
-}
-
-// Display search results
-function displaySearchResults(results) {
- searchResultsEl.innerHTML = '';
-
- if (results.length === 0) {
- searchResultsEl.innerHTML = '
- `;
-
- episodesContainer.appendChild(episodeRow);
- });
-
- seasonsContainer.appendChild(episodesContainer);
-
- // Also update the main file list to show episode matching information
- updateFileListWithEpisodeInfo(episodes);
-}
-
-// Enhanced function to create a more integrated view
-function createIntegratedEpisodeFileView(episodes) {
- // This function would create a more integrated view showing both files and episodes
- // For now, we're enhancing the existing functionality to better align the views
-
- // Add a section that shows how files might align with episodes
- const fileListContainer = document.getElementById('file-list');
-
- // Create a section showing the relationship
- const relationshipSection = document.createElement('div');
- relationshipSection.style.marginTop = '15px';
- relationshipSection.style.padding = '12px';
- relationshipSection.style.backgroundColor = '#fff8e1';
- relationshipSection.style.border = '1px solid #ffd54f';
- relationshipSection.style.borderRadius = '6px';
- relationshipSection.innerHTML = `
-
File-Episode Relationship
-
- Matching Strategy: Files are matched to episodes based on naming patterns.
-
-
- Example: "Show.S01E01.Title.mp4" matches Episode 1 of Season 1.
-
-
- Status: ${currentFiles.length} files in directory, ${episodes.length} episodes available.
-
- `;
-
- // Insert this section after the episode info
- const episodeInfo = fileListContainer.querySelector('.episode-info');
- if (episodeInfo) {
- fileListContainer.insertBefore(relationshipSection, episodeInfo.nextSibling);
- }
-}
-
-// Update file list to show episode matching information
-function updateFileListWithEpisodeInfo(episodes) {
- // When we have episodes, we want to show basic episode information
- if (currentFiles && currentFiles.length > 0 && episodes && episodes.length > 0) {
- console.log('Updating file list with episode info for', episodes.length, 'episodes');
-
- // Create a simple note about episode availability
- const fileListContainer = document.getElementById('file-list');
-
- // Add a simple note about episodes
- const episodeNote = document.createElement('div');
- episodeNote.style.marginTop = '15px';
- episodeNote.style.padding = '10px';
- episodeNote.style.backgroundColor = '#e7f3ff';
- episodeNote.style.border = '1px solid #b3d9ff';
- episodeNote.style.borderRadius = '5px';
- episodeNote.style.fontSize = '14px';
- episodeNote.innerHTML = `
- Episode Information:
- ${episodes.length} episodes available for this show.
- Episode details are displayed in the sidebar.
- `;
-
- // Add this note to the file list container
- if (fileListContainer.firstChild) {
- fileListContainer.insertBefore(episodeNote, fileListContainer.firstChild);
- } else {
- fileListContainer.appendChild(episodeNote);
- }
- }
-}
-
-// Debounce function for search input
-function debounce(func, wait) {
- let timeout;
- return function executedFunction(...args) {
- const later = () => {
- clearTimeout(timeout);
- func(...args);
- };
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- };
-}
-
-// Modal element for video preview
-let videoPreviewModal = null;
-
-// Initialize the application
-console.log('Movie Mapper application initialized');
-
-// Function to open video preview modal
-function openVideoPreview(filePath) {
- console.log('Opening video preview for:', filePath);
-
- // Create modal if it doesn't exist
- if (!videoPreviewModal) {
- createVideoPreviewModal();
- }
-
- // Show the modal
- videoPreviewModal.style.display = 'block';
-
- // Load video content
- loadVideoPreview(filePath);
-}
-
-// Function to create video preview modal
-function createVideoPreviewModal() {
- // Create modal container
- videoPreviewModal = document.createElement('div');
- videoPreviewModal.id = 'video-preview-modal';
- videoPreviewModal.style.cssText = `
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.9);
- z-index: 1000;
- justify-content: center;
- align-items: center;
- overflow: auto;
- `;
-
- // Create modal content
- const modalContent = document.createElement('div');
- modalContent.style.cssText = `
- position: relative;
- max-width: 90%;
- max-height: 90%;
- background-color: #fff;
- border-radius: 8px;
- padding: 20px;
- margin: 20px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
- `;
-
- // Create close button
- const closeButton = document.createElement('span');
- closeButton.innerHTML = '×';
- closeButton.style.cssText = `
- position: absolute;
- top: 10px;
- right: 15px;
- font-size: 30px;
- font-weight: bold;
- color: #aaa;
- cursor: pointer;
- transition: color 0.3s;
- `;
-
- closeButton.addEventListener('mouseenter', function() {
- this.style.color = '#000';
- });
-
- closeButton.addEventListener('click', function() {
- videoPreviewModal.style.display = 'none';
- });
-
- // Create video container
- const videoContainer = document.createElement('div');
- videoContainer.id = 'video-preview-container';
- videoContainer.style.cssText = `
- text-align: center;
- margin-bottom: 15px;
- `;
-
- // Create file info display
- const fileInfo = document.createElement('div');
- fileInfo.id = 'video-preview-file-info';
- fileInfo.style.cssText = `
- text-align: center;
- margin-bottom: 15px;
- padding: 10px;
- background-color: #f8f9fa;
- border-radius: 5px;
- font-size: 14px;
- `;
-
- // Create loading indicator
- const loadingIndicator = document.createElement('div');
- loadingIndicator.id = 'video-preview-loading';
- loadingIndicator.textContent = 'Loading video preview...';
- loadingIndicator.style.cssText = `
- text-align: center;
- padding: 20px;
- font-size: 16px;
- color: #666;
- `;
-
- // Assemble modal
- modalContent.appendChild(closeButton);
- modalContent.appendChild(fileInfo);
- modalContent.appendChild(videoContainer);
- modalContent.appendChild(loadingIndicator);
- videoPreviewModal.appendChild(modalContent);
-
- // Add click outside to close
- videoPreviewModal.addEventListener('click', function(e) {
- if (e.target === videoPreviewModal) {
- videoPreviewModal.style.display = 'none';
- }
- });
-
- // Add to body
- document.body.appendChild(videoPreviewModal);
-}
-
-// Function to load video preview
-async function loadVideoPreview(filePath) {
- const fileInfo = document.getElementById('video-preview-file-info');
- const videoContainer = document.getElementById('video-preview-container');
- const loadingIndicator = document.getElementById('video-preview-loading');
-
- // Show loading
- loadingIndicator.style.display = 'block';
- videoContainer.innerHTML = '';
- fileInfo.innerHTML = `Loading: ${filePath.split('/').pop()}`;
-
- try {
- // Check if file exists
const fs = require('fs');
- if (!fs.existsSync(filePath)) {
- throw new Error('File not found');
- }
-
- // Get file stats for info
- const stats = fs.statSync(filePath);
- const fileName = filePath.split('/').pop();
-
- // Update file info
- fileInfo.innerHTML = `
- File: ${fileName}
- Size: ${(stats.size / (1024 * 1024)).toFixed(2)} MB
- Path: ${filePath}
- `;
-
- // For MKV files, open directly in default player
- const ext = filePath.toLowerCase().split('.').pop();
- if (ext === 'mkv') {
- // Close the modal and open in default player directly
- videoPreviewModal.style.display = 'none';
- ipcRenderer.invoke('open-file-in-player', filePath);
+ if (fs.existsSync(oldPath) && oldPath !== newPath) {
+ fs.renameSync(oldPath, newPath);
+ return { success: true, message: 'File renamed successfully' };
} else {
- // For other formats, try to create a video player
- videoContainer.innerHTML = `
-
- `;
-
- // Hide loading
- loadingIndicator.style.display = 'none';
+ return { success: false, error: 'File not found or paths are the same' };
}
} catch (error) {
- console.error('Error loading video preview:', error);
- fileInfo.innerHTML = `Error: Could not load preview for ${filePath}`;
- videoContainer.innerHTML = '
Error loading video preview
';
- loadingIndicator.style.display = 'none';
+ return { success: false, error: error.message };
}
-}
+});
+
+console.log('Movie Mapper renderer loaded');
\ No newline at end of file
diff --git a/utils/renderer/AppState.js b/utils/renderer/AppState.js
new file mode 100644
index 0000000..c5b86d0
--- /dev/null
+++ b/utils/renderer/AppState.js
@@ -0,0 +1,143 @@
+const path = require('path');
+
+/**
+ * AppState - Manages application state
+ */
+class AppState {
+ constructor() {
+ this.currentDirectory = null;
+ this.currentFiles = [];
+ this.currentShow = null;
+ this.currentSeasons = [];
+ this.currentEpisodes = [];
+ this.selectedSeasonEpisodeCount = 0;
+ this.isUpdatingEpisodeNumbers = false;
+ }
+
+ /**
+ * Set the current directory
+ * @param {string} directory - Directory path
+ */
+ setCurrentDirectory(directory) {
+ this.currentDirectory = directory;
+ }
+
+ /**
+ * Get the current directory
+ * @returns {string|null} Current directory path
+ */
+ getCurrentDirectory() {
+ return this.currentDirectory;
+ }
+
+ /**
+ * Set current files
+ * @param {Array} files - Array of file objects
+ */
+ setCurrentFiles(files) {
+ this.currentFiles = files;
+ }
+
+ /**
+ * Get current files
+ * @returns {Array} Array of file objects
+ */
+ getCurrentFiles() {
+ return this.currentFiles;
+ }
+
+ /**
+ * Set current show
+ * @param {Object} show - Show object
+ */
+ setCurrentShow(show) {
+ this.currentShow = show;
+ }
+
+ /**
+ * Get current show
+ * @returns {Object|null} Current show object
+ */
+ getCurrentShow() {
+ return this.currentShow;
+ }
+
+ /**
+ * Set current seasons
+ * @param {Array} seasons - Array of season objects
+ */
+ setCurrentSeasons(seasons) {
+ this.currentSeasons = seasons;
+ }
+
+ /**
+ * Get current seasons
+ * @returns {Array} Array of season objects
+ */
+ getCurrentSeasons() {
+ return this.currentSeasons;
+ }
+
+ /**
+ * Set current episodes
+ * @param {Array} episodes - Array of episode objects
+ */
+ setCurrentEpisodes(episodes) {
+ this.currentEpisodes = episodes;
+ }
+
+ /**
+ * Get current episodes
+ * @returns {Array} Array of episode objects
+ */
+ getCurrentEpisodes() {
+ return this.currentEpisodes;
+ }
+
+ /**
+ * Set selected season episode count
+ * @param {number} count - Episode count
+ */
+ setSelectedSeasonEpisodeCount(count) {
+ this.selectedSeasonEpisodeCount = count;
+ }
+
+ /**
+ * Get selected season episode count
+ * @returns {number} Episode count
+ */
+ getSelectedSeasonEpisodeCount() {
+ return this.selectedSeasonEpisodeCount;
+ }
+
+ /**
+ * Check if episode numbers are currently updating
+ * @returns {boolean} True if updating
+ */
+ isUpdatingEpisodeNumbers() {
+ return this.isUpdatingEpisodeNumbers;
+ }
+
+ /**
+ * Set episode numbers updating flag
+ * @param {boolean} updating - Updating state
+ */
+ setUpdatingEpisodeNumbers(updating) {
+ this.isUpdatingEpisodeNumbers = updating;
+ }
+
+ /**
+ * Reset state to initial values
+ */
+ reset() {
+ this.currentDirectory = null;
+ this.currentFiles = [];
+ this.currentShow = null;
+ this.currentSeasons = [];
+ this.currentEpisodes = [];
+ this.selectedSeasonEpisodeCount = 0;
+ this.isUpdatingEpisodeNumbers = false;
+ }
+}
+
+module.exports = AppState;
\ No newline at end of file
diff --git a/utils/renderer/EpisodeManager.js b/utils/renderer/EpisodeManager.js
new file mode 100644
index 0000000..bda2431
--- /dev/null
+++ b/utils/renderer/EpisodeManager.js
@@ -0,0 +1,285 @@
+/**
+ * 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;
\ No newline at end of file
diff --git a/utils/renderer/FileListManager.js b/utils/renderer/FileListManager.js
new file mode 100644
index 0000000..80404b4
--- /dev/null
+++ b/utils/renderer/FileListManager.js
@@ -0,0 +1,243 @@
+/**
+ * FileListManager - Handles file list display and manipulation
+ */
+class FileListManager {
+ constructor(fileListEl) {
+ this.fileListEl = fileListEl;
+ this.draggedItem = null;
+ }
+
+ /**
+ * Display files in the UI
+ * @param {Array} files - Array of file objects
+ */
+ displayFiles(files) {
+ 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', () => {
+ // This will be handled by the caller
+ });
+
+ 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 = `
+