MovieMapper/utils/renderer/TagManager.js-e
Jarian Cottingham 5b1d24ae4a Add file editing via double-click and fix TVDB ID appending logic
- Add double-click handler to make file names editable in UI
- Remove duplicate IPC handler for rename-file in renderer.js
- Fix checkEpisodeCountMatch to properly validate sequential episodes
- Fix begin-mapping to handle both season folder and show folder cases
- Improve open-file-in-player handler to properly log results
2026-02-26 19:20:15 -06:00

257 lines
7.9 KiB
Plaintext

const { ipcRenderer } = require('electron');
/**
* TagManager - Manages file tagging functionality
*/
class TagManager {
constructor() {
this.tagColors = {
extra: '#28a745', // Green
behindTheScenes: '#17a2b8', // Teal
delete: '#dc3545' // Red
};
}
/**
* Tag a file with a specific tag type
* @param {string} filePath - File path
* @param {string} tagType - Tag type (extra, behindTheScenes, delete)
* @param {Function} callback - Callback function
*/
tagFile(filePath, tagType, callback) {
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) {
this._applyTagVisuals(item, tagType);
item.setAttribute('data-tagged-' + tagType, 'true');
this._enablePlayButton(item);
}
});
// Update the tagged count display
if (callback && typeof callback === 'function') {
callback();
}
console.log(`File ${filePath} tagged as ${tagType}`);
}
/**
* Apply visual styling for a tag
* @param {HTMLElement} fileItem - File item element
* @param {string} tagType - Tag type
* @private
*/
_applyTagVisuals(fileItem, tagType) {
const tagIcon = fileItem.querySelector(`.${tagType}-tag`);
if (tagIcon) {
const tagColor = this.tagColors[tagType];
// 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)';
}
}
/**
* Enable play button for a file
* @param {HTMLElement} fileItem - File item element
* @private
*/
_enablePlayButton(fileItem) {
const playButton = fileItem.querySelector('.play-button');
if (playButton) {
playButton.style.opacity = '1';
playButton.style.cursor = 'pointer';
playButton.disabled = false;
playButton.style.pointerEvents = 'auto';
}
}
/**
* Untag a file
* @param {string} filePath - File path
* @param {string} tagType - Tag type
*/
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) {
this._removeTagVisuals(item, tagType);
item.removeAttribute('data-tagged-' + tagType);
this._checkAndDisablePlayButton(item);
}
});
}
/**
* Remove tag visuals
* @param {HTMLElement} fileItem - File item element
* @param {string} tagType - Tag type
* @private
*/
_removeTagVisuals(fileItem, tagType) {
const tagIcon = fileItem.querySelector(`.${tagType}-tag`);
if (tagIcon) {
// Reset to original appearance
tagIcon.style.opacity = '0.7';
tagIcon.style.filter = 'none';
tagIcon.style.color = '';
tagIcon.style.textShadow = 'none';
tagIcon.style.transform = 'scale(1)';
tagIcon.style.boxShadow = 'none';
}
}
/**
* Check and disable play button if no tags remain
* @param {HTMLElement} fileItem - File item element
* @private
*/
_checkAndDisablePlayButton(fileItem) {
const hasOtherTags = fileItem.hasAttribute('data-tagged-extra') ||
fileItem.hasAttribute('data-tagged-behind-the-scenes') ||
fileItem.hasAttribute('data-tagged-delete');
if (!hasOtherTags) {
const playButton = fileItem.querySelector('.play-button');
if (playButton) {
playButton.style.opacity = '0.3';
playButton.style.cursor = 'default';
playButton.disabled = true;
playButton.style.pointerEvents = 'none';
}
}
}
/**
* Move a tagged file
* @param {string} filePath - File path
* @param {string} tagType - Tag type
* @returns {Promise<Object>} Move result
*/
async moveTaggedFile(filePath, tagType) {
console.log(`Moving file ${filePath} to ${tagType} folder`);
// Send request to main process to move the file
const result = await ipcRenderer.invoke('move-file-to-folder', {
filePath: filePath,
folderName: tagType
});
if (result.success) {
console.log(`File moved successfully to ${tagType} folder`);
return { success: true, filePath };
} else {
console.error(`Failed to move file: ${result.error}`);
return { success: false, error: result.error, filePath };
}
}
/**
* Move all tagged files
* @param {Function} onUpdateCount - Callback to update count
* @param {Function} onEpisodesUpdated - Callback to update episodes
* @param {Function} onMatchCheck - Callback to check episode match
* @returns {Promise<Object>} Results summary
*/
async moveAllTaggedFiles(onUpdateCount, onEpisodesUpdated, onMatchCheck) {
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('No tagged files to move');
return { success: true, successful: 0, failed: 0 };
}
console.log(`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 = 'behindTheScenes';
} 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
if (onUpdateCount) onUpdateCount();
if (onEpisodesUpdated) onEpisodesUpdated();
if (onMatchCheck) onMatchCheck();
// 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(`Successfully moved all ${successful} files`);
}
return { success: true, successful, failed };
}
/**
* Get all tagged files
* @returns {Array} Array of tagged file objects
*/
getTaggedFiles() {
const taggedItems = document.querySelectorAll('.file-item[data-tagged-extra], .file-item[data-tagged-behind-the-scenes], .file-item[data-tagged-delete]');
const taggedFiles = [];
taggedItems.forEach(item => {
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 = 'behindTheScenes';
} else if (item.hasAttribute('data-tagged-delete')) {
tagType = 'delete';
}
taggedFiles.push({ filePath, tagType });
});
return taggedFiles;
}
}
module.exports = TagManager;