- 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
343 lines
11 KiB
JavaScript
343 lines
11 KiB
JavaScript
/**
|
|
* FileListManager - Handles file list display and manipulation
|
|
*/
|
|
class FileListManager {
|
|
constructor(fileListEl, onFolderClick = null) {
|
|
this.fileListEl = fileListEl;
|
|
this.draggedItem = null;
|
|
this.onFolderClick = onFolderClick || null;
|
|
this.hoveredEpisodeRange = null;
|
|
this._setupHoverHandlers();
|
|
}
|
|
|
|
/**
|
|
* Set callback for folder click events
|
|
* @param {Function} callback - Function to call when folder is clicked
|
|
*/
|
|
setFolderClickCallback(callback) {
|
|
this.onFolderClick = callback;
|
|
}
|
|
|
|
/**
|
|
* Display files in the UI
|
|
* @param {Array} files - Array of file objects
|
|
*/
|
|
displayFiles(files) {
|
|
// Re-setup hover handlers when displaying files
|
|
this._setupHoverHandlers();
|
|
|
|
this.fileListEl.innerHTML = '';
|
|
|
|
if (files.length === 0) {
|
|
this.fileListEl.innerHTML = '<p>No media files found in this directory.</p>';
|
|
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 = `
|
|
<div style="font-size: 18px; margin-right: 10px;">📁</div>
|
|
<div class="file-name folder-name" data-file-path="${file.path}">${file.name}</div>
|
|
<div class="file-duration"></div>
|
|
<div class="file-quality"></div>
|
|
<div class="file-fps"></div>
|
|
<div class="file-tags"></div>
|
|
`;
|
|
|
|
// Add click handler to navigate into folder
|
|
fileItem.addEventListener('click', (e) => {
|
|
// Don't trigger if clicking on any child elements
|
|
if (e.target !== fileItem && e.target.className !== 'file-name folder-name') {
|
|
return;
|
|
}
|
|
|
|
if (this.onFolderClick) {
|
|
this.onFolderClick(file.path, file.name);
|
|
}
|
|
});
|
|
|
|
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 ? '<span class="problematic-label">⚠️</span> ' : '';
|
|
|
|
fileItem.innerHTML = `
|
|
<div class="drag-handle">⋮⋮</div>
|
|
<div class="episode-number-container">
|
|
<button class="episode-arrow episode-arrow-left" data-file-path="${file.path}" title="Decrease episode range">◀</button>
|
|
<div class="episode-number" data-episode-start="${episodeNumber}" data-episode-end="${episodeNumber}">${episodeNumber}</div>
|
|
<button class="episode-arrow episode-arrow-right" data-file-path="${file.path}" title="Increase episode range">▶</button>
|
|
</div>
|
|
<div class="file-name" data-file-path="${file.path}">${file.name}</div>
|
|
<div class="file-duration">${duration}</div>
|
|
<div class="file-quality">${quality}</div>
|
|
<div class="file-fps">${fps}</div>
|
|
<div class="file-tags">
|
|
<span class="tag-icon extra-tag" data-file-path="${file.path}" title="Mark as Extra">🏷️</span>
|
|
<span class="tag-icon behind-the-scenes-tag" data-file-path="${file.path}" title="Add Behind the Scenes">🎥</span>
|
|
<span class="tag-icon delete-tag" data-file-path="${file.path}" title="Mark for Deletion">🗑️</span>
|
|
<span class="video-preview-btn" data-file-path="${file.path}" title="Preview Video">🎬</span>
|
|
<button class="play-button" style="opacity: 0.3; cursor: default; flex-shrink: 0;" data-file-path="${file.path}" disabled>▶️</button>
|
|
</div>
|
|
`;
|
|
|
|
// Add drag and drop event listeners
|
|
fileItem.addEventListener('dragstart', this._handleDragStart.bind(this));
|
|
fileItem.addEventListener('dragend', this._handleDragEnd.bind(this));
|
|
fileItem.addEventListener('dragover', this._handleDragOver.bind(this));
|
|
fileItem.addEventListener('dragleave', this._handleDragLeave.bind(this));
|
|
fileItem.addEventListener('drop', this._handleDrop.bind(this));
|
|
|
|
this.fileListEl.appendChild(fileItem);
|
|
}
|
|
|
|
/**
|
|
* Handle drag start
|
|
* @param {Event} e - Drag event
|
|
* @private
|
|
*/
|
|
_handleDragStart(e) {
|
|
const fileItem = this;
|
|
this.draggedItem = fileItem;
|
|
fileItem.classList.add('dragging');
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
e.dataTransfer.setData('text/plain', fileItem.dataset.index);
|
|
}
|
|
|
|
/**
|
|
* Handle drag end
|
|
* @param {Event} e - Drag event
|
|
* @private
|
|
*/
|
|
_handleDragEnd(e) {
|
|
const fileItem = this;
|
|
fileItem.classList.remove('dragging');
|
|
// Remove drag-over class from all items
|
|
document.querySelectorAll('.file-item').forEach(item => {
|
|
item.classList.remove('drag-over');
|
|
});
|
|
this.draggedItem = null;
|
|
}
|
|
|
|
/**
|
|
* Handle drag over
|
|
* @param {Event} e - Drag event
|
|
* @private
|
|
*/
|
|
_handleDragOver(e) {
|
|
const fileItem = this;
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'move';
|
|
|
|
// Only show drag-over for non-folder items
|
|
if (!fileItem.classList.contains('folder-item') && fileItem !== this.draggedItem) {
|
|
fileItem.classList.add('drag-over');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle drag leave
|
|
* @param {Event} e - Drag event
|
|
* @private
|
|
*/
|
|
_handleDragLeave(e) {
|
|
const fileItem = this;
|
|
fileItem.classList.remove('drag-over');
|
|
}
|
|
|
|
/**
|
|
* Handle drop
|
|
* @param {Event} e - Drop event
|
|
*/
|
|
_handleDrop(e) {
|
|
const fileItem = this;
|
|
e.preventDefault();
|
|
fileItem.classList.remove('drag-over');
|
|
|
|
if (fileItem === this.draggedItem || fileItem.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(fileItem);
|
|
|
|
if (draggedIndex === -1 || dropIndex === -1) return;
|
|
|
|
// Move the dragged item in the DOM
|
|
if (draggedIndex < dropIndex) {
|
|
fileItem.parentNode.insertBefore(this.draggedItem, fileItem.nextSibling);
|
|
} else {
|
|
fileItem.parentNode.insertBefore(this.draggedItem, fileItem);
|
|
}
|
|
|
|
// Update episode numbers
|
|
this.updateEpisodeNumbers();
|
|
}
|
|
|
|
/**
|
|
* Update episode numbers
|
|
*/
|
|
updateEpisodeNumbers() {
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clear the file list
|
|
*/
|
|
clear() {
|
|
this.fileListEl.innerHTML = '';
|
|
}
|
|
|
|
/**
|
|
* Get all media file items
|
|
* @returns {NodeList} Media file items
|
|
*/
|
|
getMediaFileItems() {
|
|
return this.fileListEl.querySelectorAll('.file-item:not(.folder-item)');
|
|
}
|
|
|
|
/**
|
|
* Get folder items
|
|
* @returns {NodeList} Folder items
|
|
*/
|
|
getFolderItems() {
|
|
return this.fileListEl.querySelectorAll('.folder-item');
|
|
}
|
|
|
|
/**
|
|
* Set up hover handlers for file items
|
|
* @private
|
|
*/
|
|
_setupHoverHandlers() {
|
|
// Use event delegation on the file list
|
|
this.fileListEl.addEventListener('mouseenter', (e) => {
|
|
const fileItem = e.target.closest('.file-item');
|
|
if (!fileItem || fileItem.classList.contains('folder-item')) return;
|
|
|
|
const episodeEl = fileItem.querySelector('.episode-number');
|
|
if (!episodeEl) return;
|
|
|
|
const startEpisode = parseInt(episodeEl.dataset.episodeStart) || 1;
|
|
const endEpisode = parseInt(episodeEl.dataset.episodeEnd) || startEpisode;
|
|
|
|
// Store hovered range for later use
|
|
this.hoveredEpisodeRange = { start: startEpisode, end: endEpisode };
|
|
|
|
// Notify UIManager to highlight episodes
|
|
if (this.onHoverStart && typeof this.onHoverStart === 'function') {
|
|
this.onHoverStart(startEpisode, endEpisode, fileItem);
|
|
}
|
|
});
|
|
|
|
this.fileListEl.addEventListener('mouseleave', (e) => {
|
|
const fileItem = e.target.closest('.file-item');
|
|
if (!fileItem) return;
|
|
|
|
// Remove from current file item
|
|
fileItem.classList.remove('hovered-file');
|
|
|
|
// Notify UIManager to remove highlights
|
|
if (this.onHoverEnd && typeof this.onHoverEnd === 'function') {
|
|
this.onHoverEnd(fileItem);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Highlight files matching episode range
|
|
* @param {number} startEpisode - Start episode number
|
|
* @param {number} endEpisode - End episode number
|
|
*/
|
|
highlightFilesMatchingEpisode(startEpisode, endEpisode) {
|
|
const mediaFiles = this.getMediaFileItems();
|
|
mediaFiles.forEach(fileItem => {
|
|
const episodeEl = fileItem.querySelector('.episode-number');
|
|
if (!episodeEl) return;
|
|
|
|
const fileStart = parseInt(episodeEl.dataset.episodeStart) || 1;
|
|
const fileEnd = parseInt(episodeEl.dataset.episodeEnd) || fileStart;
|
|
|
|
// Check if the file's episode range overlaps with the hovered range
|
|
const overlaps = !(endEpisode < fileStart || startEpisode > fileEnd);
|
|
|
|
if (overlaps) {
|
|
fileItem.classList.add('hovered-file');
|
|
} else {
|
|
fileItem.classList.remove('hovered-file');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Remove file highlights
|
|
*/
|
|
removeFileHighlights() {
|
|
const mediaFiles = this.getMediaFileItems();
|
|
mediaFiles.forEach(fileItem => {
|
|
fileItem.classList.remove('hovered-file');
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = FileListManager; |