const { ipcRenderer } = require('electron'); const path = require('path'); const AppState = require('./AppState'); const FileListManager = require('./FileListManager'); const TagManager = require('./TagManager'); const EpisodeManager = require('./EpisodeManager'); const SearchManager = require('./SearchManager'); const FileManager = require('./FileManager'); const ModalManager = require('./ModalManager'); const ProgressManager = require('./ProgressManager'); /** * UIManager - Main coordinator for UI functionality */ class UIManager { constructor() { // Initialize state this.appState = new AppState(); this.currentDirectory = null; this.currentFiles = []; this.currentShow = null; this.currentSeasons = []; this.currentEpisodes = []; this.selectedSeasonEpisodeCount = 0; this.isUpdatingEpisodeNumbers = false; // Initialize managers this.fileManager = new FileManager(); this.modalManager = new ModalManager(); // Get DOM elements this.selectDirBtn = document.getElementById('select-dir-btn'); this.selectedDirEl = document.getElementById('selected-dir'); this.searchInput = document.getElementById('search-input'); this.searchResultsEl = document.getElementById('search-results'); this.fileListEl = document.getElementById('file-list'); this.showDetailsEl = document.getElementById('show-details'); this.progressContainer = document.getElementById('progress-container'); this.progressText = document.getElementById('progress-text'); this.progressCount = document.getElementById('progress-count'); // Initialize UI managers this.fileListManager = new FileListManager(this.fileListEl); this.tagManager = new TagManager(); this.episodeManager = new EpisodeManager(this.fileListEl); this.searchManager = new SearchManager(this.searchInput, this.searchResultsEl); this.progressManager = new ProgressManager( this.progressContainer, this.progressText, this.progressCount ); // Set up event listeners this._setupEventListeners(); } /** * Set up all event listeners * @private */ _setupEventListeners() { // Select directory button this.selectDirBtn.addEventListener('click', () => this.selectDirectory()); // Search input with debounce this.searchInput.addEventListener('input', this._debounce(() => this.searchShows(), 300)); // Add click handler for episode number editing (ranges) and tags document.addEventListener('click', (e) => { // Handle episode number editing if (e.target.classList.contains('episode-number')) { e.stopPropagation(); this.episodeManager.makeEpisodeRangeEditable(e.target); return; } // Handle episode arrow buttons if (e.target.classList.contains('episode-arrow')) { e.stopPropagation(); const direction = e.target.classList.contains('episode-arrow-left') ? 'left' : 'right'; this.handleEpisodeArrowClick(e.target, direction); return; } // Handle tag icon clicks if (e.target.classList.contains('tag-icon')) { e.stopPropagation(); const tagIcon = e.target; const tagType = tagIcon.classList.contains('extra-tag') ? 'extra' : tagIcon.classList.contains('behind-the-scenes-tag') ? 'behind-the-scenes' : tagIcon.classList.contains('delete-tag') ? 'delete' : null; if (tagType) { this.handleTagClick(tagIcon, tagType); } } }); // Add click handler for Begin Mapping button const beginMappingBtn = document.getElementById('begin-mapping-btn'); if (beginMappingBtn) { beginMappingBtn.addEventListener('click', () => this.beginMapping()); } // Add click handler for the floating circle (play button) to move all tagged files const taggedCircle = document.getElementById('tagged-circle'); if (taggedCircle) { taggedCircle.addEventListener('click', () => this.moveAllTaggedFiles()); } // Listen for scan progress updates from main process ipcRenderer.on('scan-progress', (event, { current, total, fileName }) => { this.progressManager.updateProgress(current, total, fileName); }); // Listen for auto-select directory message window.addEventListener('message', (event) => { if (event.data.type === 'auto-select-directory') { this.autoSelectDirectory(event.data.directory); } }); // Listen for auto-select directory from main process ipcRenderer.on('auto-select-directory', (event, directory) => { this.autoSelectDirectory(directory); }); // Initialize tagged count on page load this.updateTaggedCount(); } /** * Select directory function */ async selectDirectory() { try { const result = await ipcRenderer.invoke('select-directory'); if (result.success) { const directory = result.directory; this.currentDirectory = directory; this.selectedDirEl.textContent = `Selected: ${directory}`; // Log audit event for directory selection await this._logAuditEvent('select_directory', { directory: directory }); // Scan the directory for media files await this.scanDirectory(directory); } else { throw new Error(result.error); } } catch (error) { console.error('Error selecting directory:', error); alert(`Error: ${error.message}`); } } /** * Open a specific directory (for folder navigation) */ async openDirectory(directory) { try { console.log('Opening directory:', directory); // Set the current directory this.currentDirectory = directory; this.selectedDirEl.textContent = `Selected: ${directory}`; // Log audit event for directory selection await this._logAuditEvent('select_directory', { directory: directory }); // Scan the directory for media files await this.scanDirectory(directory); } catch (error) { console.error('Error opening directory:', error); alert(`Error: ${error.message}`); } } /** * Auto-select directory function */ async autoSelectDirectory(directory) { try { console.log('Auto-selecting directory:', directory); // Set the current directory this.currentDirectory = directory; this.selectedDirEl.textContent = `Selected: ${directory}`; // Log audit event for directory selection await this._logAuditEvent('select_directory', { directory: directory }); // Scan the directory for media files await this.scanDirectory(directory); } catch (error) { console.error('Error auto-selecting directory:', error); alert(`Error: ${error.message}`); } } /** * Log audit event to the main process */ logAuditEvent(action, details) { if (this.currentDirectory) { ipcRenderer.invoke('log-audit-event', { directoryPath: this.currentDirectory, action: action, details: details }).catch(error => { console.error('Failed to log audit event:', error); }); } } /** * Scan directory for media files */ async scanDirectory(directoryPath) { try { // Show progress bar this.progressManager.showProgress(); const result = await ipcRenderer.invoke('scan-directory', directoryPath); // Hide progress bar this.progressManager.hideProgress(); if (result.success) { this.currentFiles = result.files; this.fileListManager.displayFiles(result.files, this.currentDirectory); } else { throw new Error(result.error); } } catch (error) { this.progressManager.hideProgress(); console.error('Error scanning directory:', error); alert(`Error scanning directory: ${error.message}`); } } /** * Show progress spinner */ showProgress() { if (this.progressContainer) { this.progressContainer.style.display = 'block'; this.progressText.textContent = 'Scanning directory...'; this.progressCount.textContent = ''; } } /** * Hide progress spinner */ hideProgress() { if (this.progressContainer) { this.progressContainer.style.display = 'none'; } } /** * Update progress display */ updateProgress(current, total, fileName) { if (this.progressContainer) { const displayCurrent = current + 1; this.progressText.textContent = `Processing: ${fileName}`; this.progressCount.textContent = `${displayCurrent} of ${total} files`; } } /** * Display search results */ displaySearchResults(results) { this.searchResultsEl.innerHTML = ''; if (results.length === 0) { this.searchResultsEl.innerHTML = '

No shows found.

'; return; } results.forEach(show => { const resultItem = document.createElement('div'); resultItem.className = 'search-result-item'; // Create a more detailed display with separate elements const showName = show.seriesName || show.name; const showYear = show.firstAired ? show.firstAired.split('-')[0] : ''; resultItem.innerHTML = `
${showName}
${showYear ? `
${showYear}
` : ''} `; resultItem.addEventListener('click', () => this.selectShow(show)); this.searchResultsEl.appendChild(resultItem); }); } /** * Search shows function */ async searchShows() { const query = this.searchInput.value.trim(); if (query.length < 2) { this.searchResultsEl.innerHTML = ''; return; } try { const result = await ipcRenderer.invoke('search-tvdb', query); if (result.success) { this.displaySearchResults(result.results); } else { throw new Error(result.error); } } catch (error) { console.error('Error searching shows:', error); this.searchResultsEl.innerHTML = `

Error: ${error.message}

`; } } /** * Clear search results when a show is selected */ clearSearchResults() { this.searchResultsEl.innerHTML = ''; } /** * Select a show and display more details */ async selectShow(show) { console.log('Selected show:', show); this.currentShow = show; // Clear search results when a show is selected this.clearSearchResults(); // Display show title const showTitleEl = document.getElementById('show-title'); if (showTitleEl) { showTitleEl.textContent = show.seriesName || show.name; } // Fetch show details including seasons try { const result = await ipcRenderer.invoke('get-show-details', show.id); if (result.success && result.data) { const showData = result.data; this.currentSeasons = showData.seasons || []; // Display seasons if (this.currentSeasons.length === 0) { const seasonsContainer = document.getElementById('seasons-container'); if (seasonsContainer) { seasonsContainer.innerHTML = '

Seasons will be loaded when you click on a season.

'; } } else { this.displaySeasons(this.currentSeasons); } } else { console.error('Failed to fetch show details:', result.error); const seasonsContainer = document.getElementById('seasons-container'); if (seasonsContainer) { seasonsContainer.innerHTML = '

Error loading seasons: ' + (result.error || 'Unknown error') + '

'; } } } catch (error) { console.error('Error fetching show details:', error); const seasonsContainer = document.getElementById('seasons-container'); if (seasonsContainer) { seasonsContainer.innerHTML = '

Error loading seasons: ' + error.message + '

'; } } } /** * Display seasons with episode counts */ displaySeasons(seasons) { const seasonsContainer = document.getElementById('seasons-container'); if (!seasonsContainer) return; seasonsContainer.innerHTML = ''; if (!seasons || seasons.length === 0) { seasonsContainer.innerHTML = '

No seasons available.

'; return; } seasons.forEach(season => { const seasonItem = document.createElement('div'); seasonItem.className = 'season-item'; // Format season display with episode count let seasonDisplay = `Season ${season.number}`; if (season.type && season.type !== 'Unknown') { seasonDisplay += ` (${season.type})`; } const episodeCount = season.episodeCount || 0; seasonItem.innerHTML = `
${seasonDisplay} ${episodeCount} eps
`; // Add click event to fetch episodes seasonItem.addEventListener('click', () => { // Remove selected class from all seasons document.querySelectorAll('.season-item').forEach(item => item.classList.remove('selected')); // Add selected class to clicked season seasonItem.classList.add('selected'); this.fetchAndDisplayEpisodes(this.currentShow.id, season.number); }); seasonsContainer.appendChild(seasonItem); }); } /** * Fetch and display episodes for a season */ async fetchAndDisplayEpisodes(showId, seasonNumber) { try { // Show loading message while fetching const seasonsContainer = document.getElementById('seasons-container'); if (seasonsContainer) { seasonsContainer.innerHTML = '

Loading episodes...

'; } const result = await ipcRenderer.invoke('get-season-episodes', showId, seasonNumber); if (result.success && result.data) { const episodes = result.data.episodes || []; this.displayEpisodes(episodes); } else { console.error('Failed to fetch episodes:', result.error); if (seasonsContainer) { seasonsContainer.innerHTML = '

Error loading episodes: ' + (result.error || 'Unknown error') + '

'; } } } catch (error) { console.error('Error fetching episodes:', error); // Show a more user-friendly error message const seasonsContainer = document.getElementById('seasons-container'); if (seasonsContainer) { seasonsContainer.innerHTML = `

Error loading episodes: ${error.message || 'Failed to load episodes'}

Note: This may be due to API limitations with the TVDB v4 API.

`; } } } /** * Display episodes in clean table format */ displayEpisodes(episodes) { const seasonsContainer = document.getElementById('seasons-container'); if (!seasonsContainer) return; seasonsContainer.innerHTML = ''; if (!episodes || episodes.length === 0) { seasonsContainer.innerHTML = '

No episodes available.

'; this.selectedSeasonEpisodeCount = 0; this.checkEpisodeCountMatch(); return; } // Store the episode count for this season this.selectedSeasonEpisodeCount = episodes.length; this.checkEpisodeCountMatch(); // Create a back button to return to seasons const backButton = document.createElement('div'); backButton.className = 'season-item'; backButton.innerHTML = '← Back to Seasons'; backButton.addEventListener('click', () => { this.selectedSeasonEpisodeCount = 0; this.checkEpisodeCountMatch(); this.displaySeasons(this.currentSeasons); }); seasonsContainer.appendChild(backButton); // Create episodes container const episodesContainer = document.createElement('div'); episodesContainer.id = 'episodes-container'; episodes.forEach(episode => { const episodeRow = document.createElement('div'); episodeRow.className = 'episode-item'; // Format episode display const episodeName = episode.name || 'Untitled'; const episodeRuntime = episode.runtime ? `${episode.runtime}m` : ''; episodeRow.innerHTML = `
E${episode.number} ${episodeName} ${episodeRuntime ? `${episodeRuntime}` : ''}
`; episodesContainer.appendChild(episodeRow); }); seasonsContainer.appendChild(episodesContainer); // Also update the main file list to show episode matching information this.updateFileListWithEpisodeInfo(episodes); } /** * Update file list to show episode matching information */ updateFileListWithEpisodeInfo(episodes) { // When we have episodes, we want to show basic episode information if (this.currentFiles && this.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 = this.fileListEl; // 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); } } } /** * Drag and drop handlers */ handleDragStart(e) { this.draggedItem = this; this.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', this.dataset.index); } handleDragEnd(e) { this.classList.remove('dragging'); // Remove drag-over class from all items document.querySelectorAll('.file-item').forEach(item => { item.classList.remove('drag-over'); }); this.draggedItem = null; } handleDragOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; // Only show drag-over for non-folder items if (!this.classList.contains('folder-item') && this !== this.draggedItem) { this.classList.add('drag-over'); } } handleDragLeave(e) { this.classList.remove('drag-over'); } handleDrop(e) { e.preventDefault(); this.classList.remove('drag-over'); if (this === this.draggedItem || this.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(this); if (draggedIndex === -1 || dropIndex === -1) return; // Move the dragged item in the DOM if (draggedIndex < dropIndex) { this.fileListEl.insertBefore(this.draggedItem, this.nextSibling); } else { this.fileListEl.insertBefore(this.draggedItem, this); } // Update episode numbers this.updateEpisodeNumbers(); } /** * Update episode numbers after reordering */ updateEpisodeNumbers() { if (this.isUpdatingEpisodeNumbers) { console.log('[updateEpisodeNumbers] Already updating, skipping re-entrant call'); return; } this.isUpdatingEpisodeNumbers = true; try { 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; } }); } finally { this.isUpdatingEpisodeNumbers = false; } } /** * Make a file name editable */ async 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); } /** * Handle episode arrow button clicks */ 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(this.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(); } /** * Handle tag icon clicks * @param {HTMLElement} tagIcon - The clicked tag icon element * @param {string} tagType - The tag type (extra, behind-the-scenes, delete) */ handleTagClick(tagIcon, tagType) { console.log(`Tag click: ${tagType}`); // Get the file name element from the tag icon's parent const fileItem = tagIcon.closest('.file-item'); const fileNameEl = fileItem.querySelector('.file-name'); if (!fileNameEl) return; const filePath = fileNameEl.dataset.filePath; console.log(`Tagging file: ${filePath} as ${tagType}`); // Add or remove the tag if (fileItem.hasAttribute(`data-tagged-${tagType}`)) { // Untag if already tagged console.log(`Untagging file: ${filePath} from ${tagType}`); this.untagFile(filePath, tagType); } else { // Tag the file console.log(`Tagging file: ${filePath} as ${tagType}`); this.addTagToEpisode(filePath, tagType); } } /** * Function to handle tagging */ 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 === 'behind-the-scenes') { 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 this.updateTaggedCount(); } /** * Function to handle moving tagged files (also logs audit) */ async moveTaggedFile(filePath, tagType) { console.log(`[RENDERER] Moving file ${filePath} to ${tagType} folder`); // Log audit event this.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 this.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 moveAllTaggedFiles() { const taggedItems = document.querySelectorAll('.file-item[data-tagged-extra], .file-item[data-tagged-behind-the-scenes], .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-behind-the-scenes')) { tagType = 'behind-the-scenes'; } else if (item.hasAttribute('data-tagged-delete')) { tagType = 'delete'; } const result = await this.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 this.updateTaggedCount(); this.updateEpisodeNumbers(); this.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 beginMapping() { const btn = document.getElementById('begin-mapping-btn'); // Get all media file items (not folders) const fileItems = this.fileListEl.querySelectorAll('.file-item:not(.folder-item)'); if (fileItems.length === 0) { alert('No media files to map. Please select a directory first.'); return; } if (!this.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 = this.currentShow ? this.currentShow.id : null; try { const result = await ipcRenderer.invoke('begin-mapping', { directory: this.currentDirectory, files: filesToMap, tvdbId: tvdbId }); if (result.success) { alert('Mapping Complete!'); // Update currentDirectory if it changed (show folder was renamed) const newDir = result.newDirectory || this.currentDirectory; this.currentDirectory = newDir; // Refresh the file list to show new names this.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 */ 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 === 'behind the scenes') { displayName = 'behind the scenes'; 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 (this.fileListEl.firstChild) { this.fileListEl.insertBefore(folderItem, this.fileListEl.firstChild); } else { this.fileListEl.appendChild(folderItem); } console.log(`Folder "${displayName}" displayed in UI`); } /** * Function to show visual feedback for directory creation */ showDirectoryFeedback(folderName) { console.log(`Directory "${folderName}" created and file moved`); // Display the folder in the UI this.displayCreatedFolder(folderName); } /** * Function to handle untagging */ 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-behind-the-scenes') || 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 this.updateTaggedCount(); } /** * Function to update the tagged items count display */ updateTaggedCount() { const taggedItems = document.querySelectorAll('.file-item[data-tagged-extra], .file-item[data-tagged-behind-the-scenes], .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 */ checkEpisodeCountMatch() { const fileList = this.fileListEl; // Count only actual media files (not folders) const mediaFiles = fileList.querySelectorAll('.file-item:not(.folder-item)'); // Calculate total episode range (sum of all episode ranges) 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); // Track the last episode end for sequential checking if (index === 0 || episodeEnd > lastEpisodeEnd) { lastEpisodeEnd = episodeEnd; } } }); // Use lastEpisodeEnd for comparison (handles ranges properly) if (this.selectedSeasonEpisodeCount > 0 && lastEpisodeEnd === this.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 = ''; } } /** * Test TVDB API connectivity */ async 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); } } /** * Debug function to log problematic file info */ async 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); } } /** * Open video preview modal */ async openVideoPreview(filePath) { console.log('Opening video preview for:', filePath); // Create modal if it doesn't exist if (!this.videoPreviewModal) { this.createVideoPreviewModal(); } // Show the modal this.videoPreviewModal.style.display = 'block'; // Load video content await this.loadVideoPreview(filePath); } /** * Create video preview modal */ createVideoPreviewModal() { // Create modal container this.videoPreviewModal = document.createElement('div'); this.videoPreviewModal.id = 'video-preview-modal'; this.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() { if (this.parentNode) { this.parentNode.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); this.videoPreviewModal.appendChild(modalContent); // Add click outside to close this.videoPreviewModal.addEventListener('click', function(e) { if (e.target === this) { this.style.display = 'none'; } }.bind(this)); // Add to body document.body.appendChild(this.videoPreviewModal); } /** * Load video preview */ async 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'; if (videoContainer) videoContainer.innerHTML = ''; if (fileInfo) 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 if (fileInfo) { 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 if (this.videoPreviewModal) { this.videoPreviewModal.style.display = 'none'; } ipcRenderer.invoke('open-file-in-player', filePath); } else { // For other formats, try to create a video player if (videoContainer) { videoContainer.innerHTML = ` `; } // Hide loading if (loadingIndicator) loadingIndicator.style.display = 'none'; } } catch (error) { console.error('Error loading video preview:', error); if (fileInfo) { fileInfo.innerHTML = `Error: Could not load preview for ${filePath}`; } if (videoContainer) { videoContainer.innerHTML = '

Error loading video preview

'; } if (loadingIndicator) loadingIndicator.style.display = 'none'; } } /** * Debounce function for search input */ _debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * Log audit event */ async _logAuditEvent(action, details) { await this.logAuditEvent(action, details); } } module.exports = UIManager;