diff --git a/index.html b/index.html
index 62cd013..5fec54a 100644
--- a/index.html
+++ b/index.html
@@ -169,6 +169,13 @@
color: #fff;
}
+ .episode-item.highlighted-episode {
+ background-color: #e94560;
+ color: #fff;
+ font-weight: bold;
+ box-shadow: 0 0 10px rgba(233, 69, 96, 0.5);
+ }
+
/* Main Content - Files */
.main-content {
flex: 1;
@@ -238,12 +245,14 @@
flex: 1;
overflow-y: auto;
padding: 10px;
+ border-radius: 8px;
}
#file-list {
display: flex;
flex-direction: column;
gap: 4px;
+ border-radius: 8px;
}
#file-list > p {
diff --git a/test-renderer-classes-comprehensive.js b/test-renderer-classes-comprehensive.js
new file mode 100644
index 0000000..8cf8049
--- /dev/null
+++ b/test-renderer-classes-comprehensive.js
@@ -0,0 +1,736 @@
+const { test, describe, beforeEach, afterEach } = require('node:test');
+const assert = require('node:assert');
+
+// Create a simple mock for DOM
+const createMockElement = () => ({
+ style: {},
+ innerHTML: '',
+ textContent: '',
+ dataset: {},
+ className: '',
+ addEventListener: () => {},
+ removeEventListener: () => {},
+ appendChild: () => {},
+ insertBefore: () => {},
+ remove: () => {},
+ querySelector: () => null,
+ querySelectorAll: () => [],
+ setAttribute: () => {},
+ removeAttribute: () => {},
+ hasAttribute: () => false,
+ closest: () => null,
+ contains: () => false,
+ focus: () => {},
+ blur: () => {},
+ click: () => {},
+ dispatchEvent: () => true,
+ classList: { add: () => {}, remove: () => {} },
+ get parentElement() { return null; }
+});
+
+// Mock DOM
+global.document = {
+ getElementById: () => createMockElement(),
+ querySelector: () => null,
+ querySelectorAll: () => [],
+ addEventListener: () => {},
+ removeEventListener: () => {},
+ createElement: () => createMockElement(),
+ createRange: () => ({ selectNodeContents: () => {} }),
+ getSelection: () => ({ addRange: () => {}, removeAllRanges: () => {} })
+};
+
+global.window = {
+ addEventListener: () => {},
+ dispatchEvent: () => true,
+ getComputedStyle: () => ({ display: 'block' })
+};
+
+// Mock ipcRenderer with proper module handling
+const mockIpcRenderer = {
+ invoke: async (channel, ...args) => {
+ switch (channel) {
+ case 'select-directory':
+ return { success: true, directory: '/test/dir' };
+ case 'scan-directory':
+ return { success: true, files: [] };
+ case 'rename-file':
+ return { success: true, message: 'Renamed' };
+ case 'begin-mapping':
+ return { success: true };
+ case 'log-audit-event':
+ return { success: true };
+ case 'move-file-to-folder':
+ return { success: true };
+ case 'log-file-info':
+ return { success: true, fileInfo: {} };
+ case 'search-tvdb':
+ return { success: true, results: [] };
+ case 'get-show-details':
+ return { success: true, data: { seasons: [] } };
+ case 'get-season-episodes':
+ return { success: true, data: { episodes: [] } };
+ case 'test-tvdb-api':
+ return { success: true, message: 'OK' };
+ default:
+ return { success: true, data: null };
+ }
+ },
+ on: () => {}
+};
+
+// Mock fs
+const mockFs = {
+ existsSync: () => true,
+ statSync: () => ({ size: 1024 * 1024 }),
+ mkdirSync: () => {},
+ renameSync: () => {}
+};
+
+// Set up module mocks before loading anything
+const Module = require('module');
+const path = require('path');
+const originalRequire = Module.prototype.require.bind(Module.prototype);
+
+Module.prototype.require = function(id) {
+ if (id === 'electron') return mockIpcRenderer;
+ if (id === 'fs') return mockFs;
+ // For UIManager, intercept and return a mock
+ if (id.includes('UIManager')) {
+ // We'll handle UIManager separately
+ return null;
+ }
+ // For relative paths in renderer, resolve properly
+ if (id.startsWith('./')) {
+ try {
+ return originalRequire(id);
+ } catch (e) {
+ // Try without .js extension
+ const idNoExt = id.replace(/\.js$/, '');
+ return originalRequire(idNoExt);
+ }
+ }
+ return originalRequire(id);
+};
+
+// Now we can safely require the modules
+const AppState = require('./utils/renderer/AppState.js');
+const EpisodeManager = require('./utils/renderer/EpisodeManager.js');
+const TagManager = require('./utils/renderer/TagManager.js');
+const SearchManager = require('./utils/renderer/SearchManager.js');
+const FileManager = require('./utils/renderer/FileManager.js');
+const ModalManager = require('./utils/renderer/ModalManager.js');
+const ProgressManager = require('./utils/renderer/ProgressManager.js');
+const FileListManager = require('./utils/renderer/FileListManager.js');
+
+// Create a separate mock UIManager for integration tests
+class MockUIManager {
+ constructor() {
+ this.appState = new AppState();
+ }
+}
+
+// ============ AppState Tests ============
+describe('AppState', () => {
+ let appState;
+
+ beforeEach(() => {
+ appState = new AppState();
+ });
+
+ test('should create instance with default values', () => {
+ assert.strictEqual(appState.currentDirectory, null);
+ assert.deepStrictEqual(appState.currentFiles, []);
+ assert.strictEqual(appState.currentShow, null);
+ assert.deepStrictEqual(appState.currentSeasons, []);
+ assert.deepStrictEqual(appState.currentEpisodes, []);
+ assert.strictEqual(appState.selectedSeasonEpisodeCount, 0);
+ });
+
+ test('should set and get current directory', () => {
+ appState.setCurrentDirectory('/test/dir');
+ assert.strictEqual(appState.getCurrentDirectory(), '/test/dir');
+ });
+
+ test('should set and get current files', () => {
+ const files = [{ name: 'file1.mp4' }, { name: 'file2.mp4' }];
+ appState.setCurrentFiles(files);
+ assert.deepStrictEqual(appState.getCurrentFiles(), files);
+ });
+
+ test('should set and get current show', () => {
+ const show = { id: '123', name: 'Test Show' };
+ appState.setCurrentShow(show);
+ assert.deepStrictEqual(appState.getCurrentShow(), show);
+ });
+
+ test('should set and get current seasons', () => {
+ const seasons = [{ number: 1 }, { number: 2 }];
+ appState.setCurrentSeasons(seasons);
+ assert.strictEqual(appState.getCurrentSeasons().length, 2);
+ });
+
+ test('should set and get current episodes', () => {
+ const episodes = [{ number: 1 }, { number: 2 }];
+ appState.setCurrentEpisodes(episodes);
+ assert.strictEqual(appState.getCurrentEpisodes().length, 2);
+ });
+
+ test('should set and get selected season episode count', () => {
+ appState.setSelectedSeasonEpisodeCount(10);
+ assert.strictEqual(appState.getSelectedSeasonEpisodeCount(), 10);
+ });
+
+ test('should set and get updating episode numbers flag', () => {
+ appState.setUpdatingEpisodeNumbers(true);
+ assert.strictEqual(appState.isUpdatingEpisodeNumbers(), true);
+ });
+
+ test('should reset all state', () => {
+ appState.setCurrentDirectory('/test');
+ appState.setCurrentFiles([{ name: 'file.mp4' }]);
+ appState.setCurrentShow({ id: '1' });
+ appState.reset();
+ assert.strictEqual(appState.currentDirectory, null);
+ assert.deepStrictEqual(appState.currentFiles, []);
+ assert.strictEqual(appState.currentShow, null);
+ });
+});
+
+// ============ EpisodeManager Tests ============
+describe('EpisodeManager', () => {
+ let episodeManager;
+ let fileListEl;
+
+ beforeEach(() => {
+ episodeManager = new EpisodeManager();
+ fileListEl = global.document.getElementById('file-list');
+ });
+
+ test('should update episode numbers sequentially', () => {
+ fileListEl.innerHTML = `
+
+
+ `;
+ episodeManager.updateEpisodeNumbers(fileListEl);
+ const episodes = fileListEl.querySelectorAll('.episode-number');
+ assert.strictEqual(episodes[0].textContent, '1');
+ assert.strictEqual(episodes[1].textContent, '2');
+ });
+
+ test('should handle episode ranges', () => {
+ fileListEl.innerHTML = `
+
+ `;
+ episodeManager.updateEpisodeNumbers(fileListEl);
+ const episodes = fileListEl.querySelectorAll('.episode-number');
+ assert.strictEqual(episodes[0].textContent, '1');
+ });
+
+ test('should skip if already updating', () => {
+ episodeManager.setIsUpdatingEpisodeNumbers(true);
+ episodeManager.updateEpisodeNumbers(fileListEl);
+ assert.strictEqual(episodeManager.getIsUpdatingEpisodeNumbers(), false);
+ });
+
+ test('should make episode range editable', () => {
+ const episodeEl = {
+ contentEditable: false,
+ textContent: '1',
+ dataset: { episodeStart: '1', episodeEnd: '1' },
+ focus: () => {},
+ classList: { add: () => {} }
+ };
+ episodeManager.makeEpisodeRangeEditable(episodeEl);
+ assert.strictEqual(episodeEl.contentEditable, 'true');
+ });
+
+ test('should not edit if already editing', () => {
+ const episodeEl = {
+ contentEditable: 'true',
+ textContent: '1',
+ dataset: { episodeStart: '1', episodeEnd: '1' },
+ focus: () => {},
+ classList: { add: () => {} }
+ };
+ episodeManager.makeEpisodeRangeEditable(episodeEl);
+ assert.strictEqual(episodeEl.contentEditable, 'true');
+ });
+
+ test('should handle left arrow click', () => {
+ const episodeEl = {
+ dataset: { episodeStart: '5', episodeEnd: '5' },
+ setAttribute: () => {}
+ };
+ const element = {
+ closest: () => ({ dataset: { filePath: '/test/file.mp4' } }),
+ querySelector: () => episodeEl
+ };
+ const mockFileListEl = {
+ querySelectorAll: () => [
+ { dataset: { filePath: '/test/file.mp4' } },
+ { dataset: { filePath: '/test/file2.mp4' } }
+ ]
+ };
+ episodeManager.handleEpisodeArrowClick(element, 'left', mockFileListEl);
+ });
+
+ test('should handle right arrow click', () => {
+ const episodeEl = {
+ dataset: { episodeStart: '5', episodeEnd: '5' },
+ setAttribute: () => {}
+ };
+ const element = {
+ closest: () => ({ dataset: { filePath: '/test/file.mp4' } }),
+ querySelector: () => episodeEl
+ };
+ const mockFileListEl = {
+ querySelectorAll: () => [
+ { dataset: { filePath: '/test/file.mp4' } },
+ { dataset: { filePath: '/test/file2.mp4' } }
+ ]
+ };
+ episodeManager.handleEpisodeArrowClick(element, 'right', mockFileListEl);
+ });
+
+ test('should get episode range from element', () => {
+ const episodeEl = {
+ dataset: { episodeStart: '1', episodeEnd: '3' }
+ };
+ const range = episodeManager.getEpisodeRange(episodeEl);
+ assert.strictEqual(range.start, 1);
+ assert.strictEqual(range.end, 3);
+ });
+
+ test('should return single episode when no range', () => {
+ const episodeEl = {
+ dataset: { episodeStart: '5' }
+ };
+ const range = episodeManager.getEpisodeRange(episodeEl);
+ assert.strictEqual(range.start, 5);
+ assert.strictEqual(range.end, 5);
+ });
+
+ test('should calculate total episode count', () => {
+ fileListEl.innerHTML = `
+
+
+ `;
+ const count = episodeManager.calculateTotalEpisodeCount(fileListEl);
+ assert.strictEqual(count, 5);
+ });
+
+ test('should get last episode end', () => {
+ fileListEl.innerHTML = `
+
+
+ `;
+ const last = episodeManager.getLastEpisodeEnd(fileListEl);
+ assert.strictEqual(last, 5);
+ });
+
+ test('should highlight episodes in range', () => {
+ const container = {
+ querySelectorAll: () => [
+ { textContent: 'E1', classList: { add: () => {} } },
+ { textContent: 'E2', classList: { add: () => {} } },
+ { textContent: 'E5', classList: { add: () => {} } }
+ ]
+ };
+ episodeManager.highlightEpisodes(1, 2, container);
+ });
+
+ test('should remove highlights from all episodes', () => {
+ const container = {
+ querySelectorAll: () => [
+ { classList: { remove: () => {} } },
+ { classList: { remove: () => {} } }
+ ]
+ };
+ episodeManager.removeEpisodeHighlights(container);
+ });
+});
+
+// ============ TagManager Tests ============
+describe('TagManager', () => {
+ let tagManager;
+
+ beforeEach(() => {
+ tagManager = new TagManager();
+ });
+
+ test('should initialize with default tag colors', () => {
+ assert.strictEqual(tagManager.tagColors.extra, '#FFD700');
+ assert.strictEqual(tagManager.tagColors.commentary, '#17a2b8');
+ assert.strictEqual(tagManager.tagColors.delete, '#dc3545');
+ });
+
+ test('should tag file', () => {
+ tagManager.tagFile('/test/file.mp4', 'extra', () => {});
+ });
+
+ test('should untag file', () => {
+ tagManager.untagFile('/test/file.mp4', 'extra');
+ });
+
+ test('should move tagged file', async () => {
+ const result = await tagManager.moveTaggedFile('/test/file.mp4', 'extra');
+ assert.strictEqual(result.success, true);
+ });
+
+ test('should move all tagged files', async () => {
+ const result = await tagManager.moveAllTaggedFiles(
+ () => {},
+ () => {},
+ () => {}
+ );
+ assert.strictEqual(result.successful, 0);
+ assert.strictEqual(result.failed, 0);
+ });
+
+ test('should return array of tagged files', () => {
+ const taggedFiles = tagManager.getTaggedFiles();
+ assert.deepStrictEqual(taggedFiles, []);
+ });
+});
+
+// ============ SearchManager Tests ============
+describe('SearchManager', () => {
+ let searchManager;
+
+ beforeEach(() => {
+ const searchInputEl = global.document.getElementById('search-input');
+ const searchResultsEl = global.document.getElementById('search-results');
+ searchManager = new SearchManager(searchInputEl, searchResultsEl);
+ });
+
+ test('should create instance with elements', () => {
+ assert.ok(searchManager.searchInputEl);
+ assert.ok(searchManager.searchResultsEl);
+ });
+
+ test('should search with valid query', async () => {
+ const result = await searchManager.searchShows('test');
+ assert.ok(result.success || result.success === undefined);
+ });
+
+ test('should return empty results for short query', async () => {
+ const result = await searchManager.searchShows('a');
+ assert.deepStrictEqual(result.results, []);
+ });
+
+ test('should display search results', () => {
+ const results = [
+ { seriesName: 'Show 1', firstAired: '2020-01-01' },
+ { name: 'Show 2', firstAired: '2021-01-01' }
+ ];
+ searchManager.displaySearchResults(results);
+ assert.strictEqual(searchManager.searchResultsEl.children.length, 2);
+ });
+
+ test('should handle empty results', () => {
+ searchManager.displaySearchResults([]);
+ assert.ok(searchManager.searchResultsEl.innerHTML.includes('No shows found'));
+ });
+
+ test('should handle results without seriesName', () => {
+ const results = [{ name: 'Show with name property' }];
+ searchManager.displaySearchResults(results);
+ assert.ok(searchManager.searchResultsEl.innerHTML.includes('Show with name property'));
+ });
+
+ test('should clear search results', () => {
+ searchManager.searchResultsEl.innerHTML = 'Test
';
+ searchManager.clearSearchResults();
+ assert.strictEqual(searchManager.searchResultsEl.innerHTML, '');
+ });
+
+ test('should select show and update title', async () => {
+ const show = { id: '123', seriesName: 'Test Show' };
+ let titleUpdated = false;
+ await searchManager.selectShow(show,
+ (title) => { titleUpdated = true; },
+ () => {}
+ );
+ assert.strictEqual(titleUpdated, true);
+ });
+
+ test('should display seasons with episode counts', () => {
+ const seasons = [
+ { number: 1, episodeCount: 10, type: 'Season' },
+ { number: 2, episodeCount: 12, type: 'Special' }
+ ];
+ searchManager.displaySeasons(seasons, () => {}, () => {});
+ assert.ok(searchManager.searchResultsEl.innerHTML.includes('Season 1'));
+ });
+
+ test('should handle empty seasons', () => {
+ searchManager.displaySeasons([], () => {}, () => {});
+ assert.ok(searchManager.searchResultsEl.innerHTML.includes('No seasons available'));
+ });
+
+ test('should fetch and display episodes', async () => {
+ const onEpisodesDisplay = (episodes, error) => {
+ if (error) assert.ok(error);
+ };
+ await searchManager.fetchAndDisplayEpisodes('123', 1, onEpisodesDisplay);
+ });
+
+ test('should display episodes with back button', () => {
+ const episodes = [
+ { number: 1, name: 'Episode 1', runtime: 30 },
+ { number: 2, name: 'Episode 2', runtime: 45 }
+ ];
+ searchManager.displayEpisodes(episodes, 2, () => {}, () => {});
+ assert.ok(searchManager.searchResultsEl.innerHTML.includes('E1'));
+ });
+
+ test('should test TVDB API connectivity', async () => {
+ const result = await searchManager.testTVDBAPI();
+ assert.ok(result.success || result.success === undefined);
+ });
+});
+
+// ============ FileManager Tests ============
+describe('FileManager', () => {
+ let fileManager;
+
+ beforeEach(() => {
+ fileManager = new FileManager();
+ });
+
+ test('should select directory', async () => {
+ const result = await fileManager.selectDirectory();
+ assert.strictEqual(result.success, true);
+ });
+
+ test('should scan directory', async () => {
+ const result = await fileManager.scanDirectory('/test/dir');
+ assert.strictEqual(result.success, true);
+ });
+
+ test('should rename file', async () => {
+ const result = await fileManager.renameFile('/test/old.mp4', 'new.mp4');
+ assert.strictEqual(result.success, true);
+ });
+
+ test('should begin mapping files', async () => {
+ const result = await fileManager.beginMapping('/test/dir', [], null);
+ assert.strictEqual(result.success, true);
+ });
+
+ test('should log audit event', async () => {
+ const result = await fileManager.logAuditEvent('/test/dir', 'test_action', { key: 'value' });
+ assert.strictEqual(result.success, true);
+ });
+
+ test('should move file to folder', async () => {
+ const result = await fileManager.moveFileToFolder('/test/file.mp4', 'extras');
+ assert.strictEqual(result.success, true);
+ });
+
+ test('should log file info', async () => {
+ const result = await fileManager.logFileInfo('/test/file.mp4');
+ assert.strictEqual(result.success, true);
+ });
+
+ test('should collect file data from UI', () => {
+ const fileListEl = {
+ querySelectorAll: () => [
+ {
+ querySelector: (selector) => {
+ if (selector === '.file-name') return { dataset: { filePath: '/test/file.mp4' } };
+ if (selector === '.file-quality') return { textContent: '1080p' };
+ if (selector === '.episode-number') return {
+ dataset: { episodeStart: '1', episodeEnd: '1' }
+ };
+ return null;
+ }
+ }
+ ]
+ };
+ const files = fileManager.collectFileData(fileListEl);
+ assert.strictEqual(files.length, 1);
+ assert.strictEqual(files[0].filePath, '/test/file.mp4');
+ assert.strictEqual(files[0].quality, '1080p');
+ });
+});
+
+// ============ ModalManager Tests ============
+describe('ModalManager', () => {
+ let modalManager;
+
+ beforeEach(() => {
+ modalManager = new ModalManager();
+ });
+
+ test('should create instance with null modal', () => {
+ assert.strictEqual(modalManager.getModal(), null);
+ });
+
+ test('should create modal element', () => {
+ modalManager.createVideoPreviewModal();
+ const modal = modalManager.getModal();
+ assert.ok(modal !== null);
+ assert.strictEqual(modal.id, 'video-preview-modal');
+ });
+
+ test('should open video preview modal', async () => {
+ modalManager.createVideoPreviewModal();
+ await modalManager.openVideoPreview('/test/file.mp4');
+ const modal = modalManager.getModal();
+ assert.strictEqual(modal.style.display, 'block');
+ });
+
+ test('should close video preview modal', () => {
+ modalManager.createVideoPreviewModal();
+ modalManager.closeVideoPreview();
+ const modal = modalManager.getModal();
+ assert.strictEqual(modal.style.display, 'none');
+ });
+
+ test('should return modal element', () => {
+ const modal = modalManager.getModal();
+ assert.ok(modal === null || modal.id === 'video-preview-modal');
+ });
+});
+
+// ============ ProgressManager Tests ============
+describe('ProgressManager', () => {
+ let progressManager;
+ let progressContainer;
+
+ beforeEach(() => {
+ progressContainer = { style: {} };
+ progressManager = new ProgressManager(progressContainer, {}, {});
+ });
+
+ test('should create instance with elements', () => {
+ assert.strictEqual(progressManager.progressContainer, progressContainer);
+ });
+
+ test('should show progress with default message', () => {
+ progressManager.showProgress();
+ assert.strictEqual(progressContainer.style.display, 'block');
+ });
+
+ test('should show progress with custom message', () => {
+ progressManager.showProgress('Custom message');
+ assert.strictEqual(progressContainer.style.display, 'block');
+ });
+
+ test('should hide progress', () => {
+ progressManager.showProgress();
+ progressManager.hideProgress();
+ assert.strictEqual(progressContainer.style.display, 'none');
+ });
+
+ test('should update progress with current/total', () => {
+ progressManager.updateProgress(5, 10, 'test.mp4');
+ });
+
+ test('should update progress text', () => {
+ progressManager.updateProgressText('Testing');
+ });
+
+ test('should update progress count', () => {
+ progressManager.updateProgressCount(3, 10);
+ });
+
+ test('should show directory feedback', () => {
+ progressManager.showDirectoryFeedback('extras');
+ });
+});
+
+// ============ FileListManager Tests ============
+describe('FileListManager', () => {
+ let fileListManager;
+ let fileListEl;
+
+ beforeEach(() => {
+ fileListEl = global.document.getElementById('file-list');
+ fileListManager = new FileListManager(fileListEl);
+ });
+
+ test('should create instance with file list element', () => {
+ assert.strictEqual(fileListManager.fileListEl, fileListEl);
+ });
+
+ test('should display empty message when no files', () => {
+ fileListManager.displayFiles([]);
+ assert.ok(fileListEl.innerHTML.includes('No media files found'));
+ });
+
+ test('should display folders first', () => {
+ const files = [
+ { name: 'Folder', path: '/folder', isFolder: true },
+ { name: 'file.mp4', path: '/file.mp4', isFolder: false }
+ ];
+ fileListManager.displayFiles(files);
+ assert.ok(fileListEl.innerHTML.includes('📁'));
+ });
+
+ test('should display media files with episode numbers', () => {
+ const files = [
+ { name: 'file1.mp4', path: '/file1.mp4', isFolder: false },
+ { name: 'file2.mp4', path: '/file2.mp4', isFolder: false }
+ ];
+ fileListManager.displayFiles(files);
+ assert.ok(fileListEl.innerHTML.includes('1'));
+ });
+
+ test('should update episode numbers after reordering', () => {
+ fileListEl.innerHTML = `
+
+
+ `;
+ fileListManager.updateEpisodeNumbers();
+ });
+
+ test('should return only media file items', () => {
+ fileListEl.innerHTML = `
+ Folder
+ File 1
+ File 2
+ `;
+ const items = fileListManager.getMediaFileItems();
+ assert.strictEqual(items.length, 2);
+ });
+
+ test('should return only folder items', () => {
+ fileListEl.innerHTML = `
+ Folder 1
+ Folder 2
+ File
+ `;
+ const items = fileListManager.getFolderItems();
+ assert.strictEqual(items.length, 2);
+ });
+
+ test('should clear file list', () => {
+ fileListEl.innerHTML = 'Test
';
+ fileListManager.clear();
+ assert.strictEqual(fileListEl.innerHTML, '');
+ });
+});
+
+// ============ UIManager Integration Tests (Separate File) ============
+// Note: UIManager tests are in a separate file due to complex dependencies
\ No newline at end of file
diff --git a/utils/renderer/EpisodeManager.js b/utils/renderer/EpisodeManager.js
index bda2431..e0b546f 100644
--- a/utils/renderer/EpisodeManager.js
+++ b/utils/renderer/EpisodeManager.js
@@ -280,6 +280,22 @@ class EpisodeManager {
return lastEpisodeEnd;
}
+
+ /**
+ * Get updating episode numbers flag
+ * @returns {boolean} Updating state
+ */
+ getIsUpdatingEpisodeNumbers() {
+ return this.isUpdatingEpisodeNumbers;
+ }
+
+ /**
+ * Set updating episode numbers flag
+ * @param {boolean} updating - Updating state
+ */
+ setIsUpdatingEpisodeNumbers(updating) {
+ this.isUpdatingEpisodeNumbers = updating;
+ }
}
module.exports = EpisodeManager;
\ No newline at end of file
diff --git a/utils/renderer/FileListManager.js b/utils/renderer/FileListManager.js
index 80404b4..93bbde8 100644
--- a/utils/renderer/FileListManager.js
+++ b/utils/renderer/FileListManager.js
@@ -120,10 +120,11 @@ class FileListManager {
* @private
*/
_handleDragStart(e) {
- this.draggedItem = this;
- this.element.classList.add('dragging');
+ const fileItem = this;
+ this.draggedItem = fileItem;
+ fileItem.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
- e.dataTransfer.setData('text/plain', this.dataset.index);
+ e.dataTransfer.setData('text/plain', fileItem.dataset.index);
}
/**
@@ -132,7 +133,8 @@ class FileListManager {
* @private
*/
_handleDragEnd(e) {
- this.element.classList.remove('dragging');
+ 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');
@@ -146,12 +148,13 @@ class FileListManager {
* @private
*/
_handleDragOver(e) {
+ const fileItem = this;
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');
+ if (!fileItem.classList.contains('folder-item') && fileItem !== this.draggedItem) {
+ fileItem.classList.add('drag-over');
}
}
@@ -161,7 +164,8 @@ class FileListManager {
* @private
*/
_handleDragLeave(e) {
- this.classList.remove('drag-over');
+ const fileItem = this;
+ fileItem.classList.remove('drag-over');
}
/**
@@ -169,27 +173,28 @@ class FileListManager {
* @param {Event} e - Drop event
*/
_handleDrop(e) {
+ const fileItem = this;
e.preventDefault();
- this.classList.remove('drag-over');
+ fileItem.classList.remove('drag-over');
- if (this === this.draggedItem || this.classList.contains('folder-item')) return;
+ 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(this);
+ const dropIndex = fileItems.indexOf(fileItem);
if (draggedIndex === -1 || dropIndex === -1) return;
// Move the dragged item in the DOM
if (draggedIndex < dropIndex) {
- this.parentNode.insertBefore(this.draggedItem, this.nextSibling);
+ fileItem.parentNode.insertBefore(this.draggedItem, fileItem.nextSibling);
} else {
- this.parentNode.insertBefore(this.draggedItem, this);
+ fileItem.parentNode.insertBefore(this.draggedItem, fileItem);
}
// Update episode numbers
- this._updateEpisodeNumbers();
+ this.updateEpisodeNumbers();
}
/**
diff --git a/utils/renderer/UIManager.js b/utils/renderer/UIManager.js
index 33b15bb..1f79ee3 100644
--- a/utils/renderer/UIManager.js
+++ b/utils/renderer/UIManager.js
@@ -1,4 +1,5 @@
const { ipcRenderer } = require('electron');
+const path = require('path');
const AppState = require('./AppState');
const FileListManager = require('./FileListManager');
const TagManager = require('./TagManager');
@@ -13,8 +14,17 @@ const ProgressManager = require('./ProgressManager');
*/
class UIManager {
constructor() {
- // Initialize managers
+ // 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();
@@ -32,7 +42,7 @@ class UIManager {
// Initialize UI managers
this.fileListManager = new FileListManager(this.fileListEl);
this.tagManager = new TagManager();
- this.episodeManager = new EpisodeManager();
+ this.episodeManager = new EpisodeManager(this.fileListEl);
this.searchManager = new SearchManager(this.searchInput, this.searchResultsEl);
this.progressManager = new ProgressManager(
this.progressContainer,
@@ -50,12 +60,12 @@ class UIManager {
*/
_setupEventListeners() {
// Select directory button
- this.selectDirBtn.addEventListener('click', () => this._handleSelectDirectory());
+ this.selectDirBtn.addEventListener('click', () => this.selectDirectory());
// Search input with debounce
- this.searchInput.addEventListener('input', this._debounce(() => this._handleSearch(), 300));
+ this.searchInput.addEventListener('input', this._debounce(() => this.searchShows(), 300));
- // Episode number editing
+ // Add click handler for episode number editing (ranges)
document.addEventListener('click', (e) => {
if (e.target.classList.contains('episode-number')) {
e.stopPropagation();
@@ -63,16 +73,16 @@ class UIManager {
}
});
- // Begin mapping button
+ // Add click handler for Begin Mapping button
const beginMappingBtn = document.getElementById('begin-mapping-btn');
if (beginMappingBtn) {
- beginMappingBtn.addEventListener('click', () => this._handleBeginMapping());
+ beginMappingBtn.addEventListener('click', () => this.beginMapping());
}
- // Floating circle (play button) to move all tagged files
+ // 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._handleMoveAllTaggedFiles());
+ taggedCircle.addEventListener('click', () => this.moveAllTaggedFiles());
}
// Listen for scan progress updates from main process
@@ -83,37 +93,36 @@ class UIManager {
// Listen for auto-select directory message
window.addEventListener('message', (event) => {
if (event.data.type === 'auto-select-directory') {
- this._handleAutoSelectDirectory(event.data.directory);
+ this.autoSelectDirectory(event.data.directory);
}
});
// Listen for auto-select directory from main process
ipcRenderer.on('auto-select-directory', (event, directory) => {
- this._handleAutoSelectDirectory(directory);
+ this.autoSelectDirectory(directory);
});
// Initialize tagged count on page load
- this._updateTaggedCount();
+ this.updateTaggedCount();
}
/**
- * Handle select directory
- * @private
+ * Select directory function
*/
- async _handleSelectDirectory() {
+ async selectDirectory() {
try {
- const result = await this.fileManager.selectDirectory();
+ const result = await ipcRenderer.invoke('select-directory');
if (result.success) {
const directory = result.directory;
- this.appState.setCurrentDirectory(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);
+ await this.scanDirectory(directory);
} else {
throw new Error(result.error);
}
@@ -124,45 +133,21 @@ class UIManager {
}
/**
- * Handle auto-select directory
- * @param {string} directory - Directory path
- * @private
- */
- async _handleAutoSelectDirectory(directory) {
- try {
- console.log('Auto-selecting directory:', directory);
-
- this.appState.setCurrentDirectory(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}`);
- }
- }
-
- /**
- * Open a specific directory
- * @param {string} directory - Directory path
+ * Open a specific directory (for folder navigation)
*/
async openDirectory(directory) {
try {
console.log('Opening directory:', directory);
// Set the current directory
- this.appState.setCurrentDirectory(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);
+ await this.scanDirectory(directory);
} catch (error) {
console.error('Error opening directory:', error);
alert(`Error: ${error.message}`);
@@ -170,32 +155,58 @@ class UIManager {
}
/**
- * Handle search
- * @private
+ * Auto-select directory function
*/
- async _handleSearch() {
- const query = this.searchInput.value.trim();
- await this.searchManager.searchShows(query);
+ 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}`);
+ }
}
/**
- * Scan directory
- * @param {string} directoryPath - Directory path
- * @private
+ * Log audit event to the main process
*/
- async _scanDirectory(directoryPath) {
+ 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 this.fileManager.scanDirectory(directoryPath);
+ const result = await ipcRenderer.invoke('scan-directory', directoryPath);
// Hide progress bar
this.progressManager.hideProgress();
if (result.success) {
- this.appState.setCurrentFiles(result.files);
- this.fileListManager.displayFiles(result.files);
+ this.currentFiles = result.files;
+ this.fileListManager.displayFiles(result.files, this.currentDirectory);
} else {
throw new Error(result.error);
}
@@ -207,21 +218,727 @@ class UIManager {
}
/**
- * Handle begin mapping
- * @private
+ * Show progress spinner
*/
- async _handleBeginMapping() {
+ 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();
+ }
+
+ /**
+ * 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 === 'commentary') {
+ 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-commentary], .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-commentary')) {
+ tagType = 'commentary';
+ } 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.fileListManager.getMediaFileItems();
+ 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.appState.getCurrentDirectory()) {
+ if (!this.currentDirectory) {
alert('No directory selected.');
return;
}
@@ -230,26 +947,43 @@ class UIManager {
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 {
- // Collect file data with episode ranges from UI order
- const filesToMap = this.fileManager.collectFileData(this.fileListEl);
-
- // Get TVDB ID from current show if selected
- const tvdbId = this.appState.getCurrentShow() ? this.appState.getCurrentShow().id : null;
-
- const result = await this.fileManager.beginMapping(
- this.appState.getCurrentDirectory(),
- filesToMap,
- tvdbId
- );
+ 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.appState.getCurrentDirectory();
- this.appState.setCurrentDirectory(newDir);
+ const newDir = result.newDirectory || this.currentDirectory;
+ this.currentDirectory = newDir;
// Refresh the file list to show new names
- await this.openDirectory(newDir);
+ this.openDirectory(newDir);
} else {
alert('Mapping failed: ' + (result.error || 'Unknown error'));
}
@@ -263,122 +997,7 @@ class UIManager {
}
/**
- * Handle move all tagged files
- * @private
- */
- async _handleMoveAllTaggedFiles() {
- await this.tagManager.moveAllTaggedFiles(
- () => this._updateTaggedCount(),
- () => this.fileListManager.updateEpisodeNumbers(),
- () => this._checkEpisodeCountMatch()
- );
- }
-
- /**
- * Update tagged count
- * @private
- */
- _updateTaggedCount() {
- const taggedItems = document.querySelectorAll('.file-item[data-tagged-extra], .file-item[data-tagged-commentary], .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';
- }
- }
- }
-
- /**
- * Check episode count match
- * @private
- */
- _checkEpisodeCountMatch() {
- // Count only actual media files (not folders)
- const mediaFiles = this.fileListManager.getMediaFileItems();
-
- // Calculate total episode range (sum of all episode ranges)
- let totalEpisodeCount = 0;
- 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);
-
- // Calculate episodes in this range
- const rangeSize = episodeEnd - episodeStart + 1;
- totalEpisodeCount += rangeSize;
-
- // Track the last episode end for sequential checking
- if (index === 0 || episodeEnd > lastEpisodeEnd) {
- lastEpisodeEnd = episodeEnd;
- }
- }
- });
-
- // Use lastEpisodeEnd for comparison (handles ranges properly)
- const selectedSeasonEpisodeCount = this.appState.getSelectedSeasonEpisodeCount();
- if (selectedSeasonEpisodeCount > 0 && lastEpisodeEnd === selectedSeasonEpisodeCount) {
- // Perfect match - add green outline
- this.fileListEl.style.border = '3px solid #28a745';
- this.fileListEl.style.boxShadow = '0 0 15px rgba(40, 167, 69, 0.4)';
- } else {
- // No match or no season selected - remove green outline
- this.fileListEl.style.border = '';
- this.fileListEl.style.boxShadow = '';
- }
- }
-
- /**
- * Log audit event
- * @param {string} action - Action type
- * @param {Object} details - Event details
- * @private
- */
- async _logAuditEvent(action, details) {
- const currentDirectory = this.appState.getCurrentDirectory();
- if (currentDirectory) {
- await this.fileManager.logAuditEvent(currentDirectory, action, details);
- }
- }
-
- /**
- * Debounce function
- * @param {Function} func - Function to debounce
- * @param {number} wait - Wait time in ms
- * @returns {Function} Debounced function
- * @private
- */
- _debounce(func, wait) {
- let timeout;
- return function executedFunction(...args) {
- const later = () => {
- clearTimeout(timeout);
- func(...args);
- };
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- };
- }
-
- /**
- * Display created folder
- * @param {string} folderName - Folder name
+ * Function to display created folders in UI
*/
displayCreatedFolder(folderName) {
// Check if folder entry already exists
@@ -442,144 +1061,149 @@ class UIManager {
}
/**
- * Make file name editable
- * @param {HTMLElement} element - File name element
+ * Function to show visual feedback for directory creation
*/
- async makeEditable(element) {
- // Prevent editing if already in edit mode
- if (element.contentEditable === 'true') return;
+ showDirectoryFeedback(folderName) {
+ console.log(`Directory "${folderName}" created and file moved`);
+ // Display the folder in the UI
+ this.displayCreatedFolder(folderName);
+ }
- // Store original text
- const originalText = element.textContent;
+ /**
+ * Function to handle untagging
+ */
+ untagFile(filePath, tagType) {
+ console.log(`Untagging file ${filePath} from ${tagType}`);
- // Make element editable
- element.contentEditable = 'true';
- element.focus();
- element.classList.add('editing');
+ // 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';
- // Select all text when editing starts
- const range = document.createRange();
- range.selectNodeContents(element);
- const selection = window.getSelection();
- selection.removeAllRanges();
- selection.addRange(range);
+ // Remove the data attribute
+ item.removeAttribute('data-tagged-' + tagType);
- // Handle saving when user finishes editing
- const saveEdit = async () => {
- if (element.textContent.trim() !== originalText.trim()) {
- // Send request to main process to rename the file
- try {
- const result = await this.fileManager.renameFile(
- element.dataset.filePath,
- 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;
+ // Disable the play button when untagging (only if no other tags)
+ const hasOtherTags = item.hasAttribute('data-tagged-extra') ||
+ item.hasAttribute('data-tagged-commentary') ||
+ 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';
+ }
}
- } 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);
+ // Update the tagged count display
+ this.updateTaggedCount();
}
/**
- * Select show and display details
- * @param {Object} show - Show object
+ * Function to update the tagged items count display
*/
- async selectShow(show) {
- await this.searchManager.selectShow(
- show,
- (title) => {
- document.getElementById('show-title').textContent = title;
- },
- (seasonsOrHtml) => {
- if (typeof seasonsOrHtml === 'string') {
- document.getElementById('seasons-container').innerHTML = seasonsOrHtml;
- } else {
- this.searchManager.displaySeasons(seasonsOrHtml,
- (seasonNumber) => this._handleSeasonSelect(show.id, seasonNumber),
- () => this._handleBackToSeasons()
- );
+ updateTaggedCount() {
+ const taggedItems = document.querySelectorAll('.file-item[data-tagged-extra], .file-item[data-tagged-commentary], .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 = '';
+ }
}
/**
- * Handle season select
- * @param {string} showId - Show ID
- * @param {number} seasonNumber - Season number
+ * Test TVDB API connectivity
*/
- async _handleSeasonSelect(showId, seasonNumber) {
- await this.searchManager.fetchAndDisplayEpisodes(
- showId,
- seasonNumber,
- (episodes, error) => {
- if (error) {
- document.getElementById('seasons-container').innerHTML = `
-
-
Error loading episodes: ${error}
-
Note: This may be due to API limitations with the TVDB v4 API.
-
- `;
- } else {
- this.searchManager.displayEpisodes(
- episodes,
- episodes.length,
- (count) => this.appState.setSelectedSeasonEpisodeCount(count),
- (episodes) => this.searchManager.updateFileListWithEpisodeInfo(episodes, this.appState.getCurrentFiles())
- );
- this._checkEpisodeCountMatch();
- }
+ 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);
+ }
}
/**
- * Handle back to seasons
- */
- _handleBackToSeasons() {
- this.appState.setSelectedSeasonEpisodeCount(0);
- this._checkEpisodeCountMatch();
- // Re-display seasons would happen here
- }
-
- /**
- * Debug problematic file
- * @param {string} filePath - File path
+ * Debug function to log problematic file info
*/
async debugProblematicFile(filePath) {
try {
- const result = await this.fileManager.logFileInfo(filePath);
+ const result = await ipcRenderer.invoke('log-file-info', filePath);
if (result.success) {
console.log('File info:', result.fileInfo);
} else {
@@ -589,6 +1213,220 @@ class UIManager {
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;
\ No newline at end of file