- 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
333 lines
11 KiB
JavaScript
333 lines
11 KiB
JavaScript
const { ipcRenderer } = require('electron');
|
|
|
|
/**
|
|
* SearchManager - Manages TVDB search and show selection
|
|
*/
|
|
class SearchManager {
|
|
constructor(searchInputEl, searchResultsEl) {
|
|
this.searchInputEl = searchInputEl;
|
|
this.searchResultsEl = searchResultsEl;
|
|
this.currentShow = null;
|
|
}
|
|
|
|
/**
|
|
* Search shows by query
|
|
* @param {string} query - Search query
|
|
* @returns {Promise<Object>} Search results
|
|
*/
|
|
async searchShows(query) {
|
|
if (query.length < 2) {
|
|
this.clearSearchResults();
|
|
return { success: true, results: [] };
|
|
}
|
|
|
|
try {
|
|
const result = await ipcRenderer.invoke('search-tvdb', query);
|
|
|
|
if (result.success) {
|
|
this.displaySearchResults(result.results);
|
|
return result;
|
|
} else {
|
|
throw new Error(result.error);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error searching shows:', error);
|
|
this.searchResultsEl.innerHTML = `<p>Error: ${error.message}</p>`;
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display search results
|
|
* @param {Array} results - Search results array
|
|
*/
|
|
displaySearchResults(results) {
|
|
this.searchResultsEl.innerHTML = '';
|
|
|
|
if (results.length === 0) {
|
|
this.searchResultsEl.innerHTML = '<p style="color: #888; text-align: center; padding: 10px;">No shows found.</p>';
|
|
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 = `
|
|
<div class="show-name">${showName}</div>
|
|
${showYear ? `<div class="show-year">${showYear}</div>` : ''}
|
|
`;
|
|
|
|
resultItem.addEventListener('click', () => this.selectShow(show));
|
|
this.searchResultsEl.appendChild(resultItem);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clear search results
|
|
*/
|
|
clearSearchResults() {
|
|
this.searchResultsEl.innerHTML = '';
|
|
}
|
|
|
|
/**
|
|
* Select a show
|
|
* @param {Object} show - Show object
|
|
* @param {Function} onShowTitleUpdate - Callback to update show title
|
|
* @param {Function} onSeasonsDisplay - Callback to display seasons
|
|
*/
|
|
async selectShow(show, onShowTitleUpdate, onSeasonsDisplay) {
|
|
console.log('Selected show:', show);
|
|
this.currentShow = show;
|
|
|
|
// Clear search results when a show is selected
|
|
this.clearSearchResults();
|
|
|
|
// Display show title
|
|
if (onShowTitleUpdate) {
|
|
onShowTitleUpdate(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;
|
|
const seasons = showData.seasons || [];
|
|
|
|
// Display seasons
|
|
if (onSeasonsDisplay) {
|
|
if (seasons.length === 0) {
|
|
onSeasonsDisplay('<p>Seasons will be loaded when you click on a season.</p>');
|
|
} else {
|
|
onSeasonsDisplay(seasons);
|
|
}
|
|
}
|
|
} else {
|
|
console.error('Failed to fetch show details:', result.error);
|
|
if (onSeasonsDisplay) {
|
|
onSeasonsDisplay('<p>Error loading seasons: ' + (result.error || 'Unknown error') + '</p>');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching show details:', error);
|
|
if (onSeasonsDisplay) {
|
|
onSeasonsDisplay('<p>Error loading seasons: ' + error.message + '</p>');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display seasons
|
|
* @param {Array} seasons - Seasons array
|
|
* @param {Function} onEpisodeFetch - Callback to fetch episodes
|
|
* @param {Function} onBackToSeasons - Callback to go back to seasons
|
|
*/
|
|
displaySeasons(seasons, onEpisodeFetch, onBackToSeasons) {
|
|
const seasonsContainer = document.getElementById('seasons-container');
|
|
seasonsContainer.innerHTML = '';
|
|
|
|
if (!seasons || seasons.length === 0) {
|
|
seasonsContainer.innerHTML = '<p style="color: #888;">No seasons available.</p>';
|
|
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 = `
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
<span>${seasonDisplay}</span>
|
|
<span style="background-color: #e94560; color: white; padding: 4px 8px; border-radius: 12px; font-size: 11px;">
|
|
${episodeCount} eps
|
|
</span>
|
|
</div>
|
|
`;
|
|
|
|
// 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');
|
|
if (onEpisodeFetch) {
|
|
onEpisodeFetch(season.number);
|
|
}
|
|
});
|
|
|
|
seasonsContainer.appendChild(seasonItem);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetch and display episodes
|
|
* @param {string} showId - Show ID
|
|
* @param {number} seasonNumber - Season number
|
|
* @param {Function} onEpisodesDisplay - Callback to display episodes
|
|
*/
|
|
async fetchAndDisplayEpisodes(showId, seasonNumber, onEpisodesDisplay) {
|
|
try {
|
|
// Show loading message while fetching
|
|
const seasonsContainer = document.getElementById('seasons-container');
|
|
seasonsContainer.innerHTML = '<p>Loading episodes...</p>';
|
|
|
|
const result = await ipcRenderer.invoke('get-season-episodes', showId, seasonNumber);
|
|
|
|
if (result.success && result.data) {
|
|
const episodes = result.data.episodes || [];
|
|
if (onEpisodesDisplay) {
|
|
onEpisodesDisplay(episodes);
|
|
}
|
|
} else {
|
|
console.error('Failed to fetch episodes:', result.error);
|
|
if (onEpisodesDisplay) {
|
|
onEpisodesDisplay(null, 'Error loading episodes: ' + (result.error || 'Unknown error'));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching episodes:', error);
|
|
// Show a more user-friendly error message
|
|
if (onEpisodesDisplay) {
|
|
onEpisodesDisplay(null, `Error loading episodes: ${error.message || 'Failed to load episodes'}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display episodes
|
|
* @param {Array} episodes - Episodes array
|
|
* @param {number} episodeCount - Episode count
|
|
* @param {Function} onBackToSeasons - Callback to go back to seasons
|
|
* @param {Function} onFileListUpdate - Callback to update file list
|
|
*/
|
|
displayEpisodes(episodes, episodeCount, onBackToSeasons, onFileListUpdate) {
|
|
const seasonsContainer = document.getElementById('seasons-container');
|
|
seasonsContainer.innerHTML = '';
|
|
|
|
if (!episodes || episodes.length === 0) {
|
|
seasonsContainer.innerHTML = '<p style="color: #888;">No episodes available.</p>';
|
|
if (onBackToSeasons) onBackToSeasons(0);
|
|
return;
|
|
}
|
|
|
|
if (onBackToSeasons) {
|
|
onBackToSeasons(episodes.length);
|
|
}
|
|
|
|
// 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', () => {
|
|
if (onBackToSeasons) onBackToSeasons(0);
|
|
if (onEpisodesDisplay) {
|
|
// Re-display seasons
|
|
}
|
|
});
|
|
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 = `
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
<span><strong>E${episode.number}</strong> ${episodeName}</span>
|
|
${episodeRuntime ? `<span style="color: #888; font-size: 11px;">${episodeRuntime}</span>` : ''}
|
|
</div>
|
|
`;
|
|
|
|
episodesContainer.appendChild(episodeRow);
|
|
});
|
|
|
|
seasonsContainer.appendChild(episodesContainer);
|
|
|
|
// Also update the main file list to show episode matching information
|
|
if (onFileListUpdate) {
|
|
onFileListUpdate(episodes);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update file list with episode info
|
|
* @param {Array} episodes - Episodes array
|
|
* @param {Array} currentFiles - Current files array
|
|
*/
|
|
updateFileListWithEpisodeInfo(episodes, currentFiles) {
|
|
// 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 = `
|
|
<strong>Episode Information:</strong>
|
|
${episodes.length} episodes available for this show.
|
|
<br><small>Episode details are displayed in the sidebar.</small>
|
|
`;
|
|
|
|
// Add this note to the file list container
|
|
if (fileListContainer.firstChild) {
|
|
fileListContainer.insertBefore(episodeNote, fileListContainer.firstChild);
|
|
} else {
|
|
fileListContainer.appendChild(episodeNote);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test TVDB API connectivity
|
|
* @returns {Promise<Object>} Test result
|
|
*/
|
|
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);
|
|
}
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Error testing TVDB API:', error);
|
|
alert('Error testing TVDB API: ' + error.message);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = SearchManager; |