`;
// Add click event to make file name editable
const fileNameElement = fileItem.querySelector('.file-name');
fileNameElement.addEventListener('click', function(e) {
makeEditable(e.target);
});
// 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;
const tagType = this.classList.contains('extra-tag') ? 'extra' : 'commentary';
// 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 the other type
const otherTagType = tagType === 'extra' ? 'commentary' : 'extra';
if (fileItem.hasAttribute('data-tagged-' + otherTagType)) {
untagFile(filePath, otherTagType);
}
// 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', function(e) {
e.stopPropagation();
console.log('Play button clicked for file:', file.path);
// First open the video preview
openVideoPreview(file.path);
// Then move the file to the appropriate folder based on tag type
const fileItem = this.closest('.file-item');
const tagType = fileItem.hasAttribute('data-tagged-extra') ? 'extra' :
fileItem.hasAttribute('data-tagged-commentary') ? 'commentary' : null;
console.log('Tag type determined:', tagType);
if (tagType) {
moveTaggedFile(file.path, tagType);
} else {
console.log('No tag type found for file');
}
});
fileListEl.appendChild(fileItem);
});
}
// 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) {
// Make the icon fully saturated and highlight in bright yellow with very strong visual effect
tagIcon.style.opacity = '1';
tagIcon.style.filter = 'none';
tagIcon.style.color = tagType === 'extra' ? '#FFD700' : '#17a2b8'; // Brighter yellow or teal color
tagIcon.style.textShadow = '0 0 15px rgba(255, 215, 0, 1)'; // Very bright yellow glow (for extra tags)
tagIcon.style.transform = 'scale(1.3)'; // More pronounced enlargement
tagIcon.style.boxShadow = '0 0 10px rgba(255, 215, 0, 0.8)'; // Additional glow effect
// 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 display created folders in UI
function displayCreatedFolder(folderPath) {
// This function would be called when a folder is created
// In a real implementation, this would add the folder to the UI
console.log(`Displaying created folder: ${folderPath}`);
}
// Function to move tagged files to appropriate folders
function moveTaggedFile(filePath, tagType) {
console.log(`Moving file ${filePath} to ${tagType} folder`);
// Send request to main process to move the file
ipcRenderer.invoke('move-file-to-folder', {
filePath: filePath,
folderName: tagType
}).then(result => {
if (result.success) {
console.log(`File moved successfully to ${tagType} folder`);
// Show visual feedback that directory was created
showDirectoryFeedback(tagType);
} else {
console.error(`Failed to move file: ${result.error}`);
alert(`Failed to move file: ${result.error}`);
}
}).catch(error => {
console.error('Error moving file:', error);
alert(`Error moving file: ${error.message}`);
});
}
// Function to show visual feedback for directory creation
function showDirectoryFeedback(folderName) {
console.log(`Directory "${folderName}" created and file moved`);
// In a more advanced implementation, we could update UI to show folder icon
// For now, we'll just log to console
}
// 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
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]');
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';
}
}
}
// Initialize tagged count on page load
document.addEventListener('DOMContentLoaded', function() {
updateTaggedCount();
});
// 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 = '
No shows found.
';
return;
}
results.forEach(show => {
const resultItem = document.createElement('div');
resultItem.className = 'search-result-item';
// Create a more detailed display
let resultText = show.seriesName || show.name;
if (show.firstAired) {
resultText += ` (${show.firstAired})`;
}
resultItem.textContent = resultText;
resultItem.addEventListener('click', () => selectShow(show));
searchResultsEl.appendChild(resultItem);
});
}
// Select a show and display more details
async function selectShow(show) {
console.log('Selected show:', show);
currentShow = show;
// Clear search results when a show is selected
clearSearchResults();
// Display show title
document.getElementById('show-title').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;
currentSeasons = showData.seasons || [];
// Display seasons - since we're not getting seasons from the API directly,
// we'll show a message indicating we need to fetch seasons separately
if (currentSeasons.length === 0) {
document.getElementById('seasons-container').innerHTML = '
Seasons will be loaded when you click on a season.
';
} else {
displaySeasons(currentSeasons);
}
} else {
console.error('Failed to fetch show details:', result.error);
document.getElementById('seasons-container').innerHTML = '
`;
// Add click event (could show more details)
episodeRow.addEventListener('click', () => {
alert(`Episode: ${episode.name || 'Untitled'}\nRuntime: ${episode.runtime || 'N/A'} min\nAired: ${episode.aired || 'N/A'}`);
});
episodesTable.appendChild(episodeRow);
});
seasonsContainer.appendChild(episodesTable);
// 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, we'll open in default player instead of trying to preview
const ext = filePath.toLowerCase().split('.').pop();
if (ext === 'mkv') {
// For MKV files, show message and provide option to open in default player
videoContainer.innerHTML = `
Warning: MKV files are not supported for in-app preview.
This file will open in your default media player.
`;
document.getElementById('open-in-player-btn').addEventListener('click', function() {
// Send request to main process to open file in default player
ipcRenderer.invoke('open-file-in-player', filePath);
});
} else {
// For other formats, try to create a video player
videoContainer.innerHTML = `
`;
// Hide loading
loadingIndicator.style.display = 'none';
}
} catch (error) {
console.error('Error loading video preview:', error);
fileInfo.innerHTML = `Error: Could not load preview for ${filePath}`;
videoContainer.innerHTML = '