Merge pull request 'Fix #3,#4,#5,#6,#7,#8,#9: Security hardening - Electron context isolation, XSS fixes, command injection fix' (#13) from fix/security-all into main
Reviewed-on: https://git.home.ms/jarianc/MovieMapper/pulls/13
This commit is contained in:
commit
9770a703c5
23
index.html
23
index.html
@ -637,6 +637,29 @@
|
|||||||
<span id="tagged-count">0</span>
|
<span id="tagged-count">0</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Module system polyfill for renderer (contextIsolation enabled, nodeIntegration disabled)
|
||||||
|
window.require = window.createModuleRequire();
|
||||||
|
var module = { exports: {} };
|
||||||
|
</script>
|
||||||
|
<script src="utils/renderer/AppState.js"></script>
|
||||||
|
<script>window.AppState = module.exports; module={exports:{};};</script>
|
||||||
|
<script src="utils/renderer/FileListManager.js"></script>
|
||||||
|
<script>window.FileListManager = module.exports; module={exports:{};};</script>
|
||||||
|
<script src="utils/renderer/FileManager.js"></script>
|
||||||
|
<script>window.FileManager = module.exports; module={exports:{};};</script>
|
||||||
|
<script src="utils/renderer/TagManager.js"></script>
|
||||||
|
<script>window.TagManager = module.exports; module={exports:{};};</script>
|
||||||
|
<script src="utils/renderer/EpisodeManager.js"></script>
|
||||||
|
<script>window.EpisodeManager = module.exports; module={exports:{};};</script>
|
||||||
|
<script src="utils/renderer/SearchManager.js"></script>
|
||||||
|
<script>window.SearchManager = module.exports; module={exports:{};};</script>
|
||||||
|
<script src="utils/renderer/ModalManager.js"></script>
|
||||||
|
<script>window.ModalManager = module.exports; module={exports:{};};</script>
|
||||||
|
<script src="utils/renderer/ProgressManager.js"></script>
|
||||||
|
<script>window.ProgressManager = module.exports; module={exports:{};};</script>
|
||||||
|
<script src="utils/renderer/UIManager.js"></script>
|
||||||
|
<script>window.UIManager = module.exports; module={exports:{};};</script>
|
||||||
<script src="renderer.js"></script>
|
<script src="renderer.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
53
main.js
53
main.js
@ -31,9 +31,10 @@ function createWindow() {
|
|||||||
width: 1200,
|
width: 1200,
|
||||||
height: 800,
|
height: 800,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
contextIsolation: false,
|
nodeIntegration: false,
|
||||||
enableRemoteModule: true,
|
contextIsolation: true,
|
||||||
|
sandbox: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -844,24 +845,28 @@ ipcMain.handle('move-to-extras', async (event, filePath) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// IPC handler for opening file in default player
|
// IPC handler for opening file in default player
|
||||||
|
// FIXED: Use execFile instead of exec to prevent command injection
|
||||||
ipcMain.handle('open-file-in-player', async (event, filePath) => {
|
ipcMain.handle('open-file-in-player', async (event, filePath) => {
|
||||||
try {
|
try {
|
||||||
const { exec } = require('child_process');
|
const { execFile } = require('child_process');
|
||||||
|
|
||||||
// Open file in default player based on OS
|
// Open file in default player based on OS using execFile (safe against injection)
|
||||||
let command;
|
let command, args;
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
// macOS
|
// macOS
|
||||||
command = `open "${filePath}"`;
|
command = 'open';
|
||||||
|
args = [filePath];
|
||||||
} else if (process.platform === 'win32') {
|
} else if (process.platform === 'win32') {
|
||||||
// Windows
|
// Windows
|
||||||
command = `start "" "${filePath}"`;
|
command = 'cmd.exe';
|
||||||
|
args = ['/c', 'start', '""', filePath];
|
||||||
} else {
|
} else {
|
||||||
// Linux
|
// Linux
|
||||||
command = `xdg-open "${filePath}"`;
|
command = 'xdg-open';
|
||||||
|
args = [filePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
exec(command, (error, stdout, stderr) => {
|
execFile(command, args, (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Error opening file:', error);
|
console.error('Error opening file:', error);
|
||||||
} else {
|
} else {
|
||||||
@ -876,6 +881,32 @@ ipcMain.handle('open-file-in-player', async (event, filePath) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// IPC handler for validating folder path (renderer can't access fs directly)
|
||||||
|
ipcMain.handle('validate-folder', async (event, folderPath) => {
|
||||||
|
try {
|
||||||
|
const stats = fs.statSync(folderPath);
|
||||||
|
return { success: true, exists: true, isDirectory: stats.isDirectory() };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, exists: false, isDirectory: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// IPC handler for getting file stats (renderer can't access fs directly)
|
||||||
|
ipcMain.handle('get-file-stats', async (event, filePath) => {
|
||||||
|
try {
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
size: stats.size,
|
||||||
|
mtime: stats.mtime,
|
||||||
|
isFile: stats.isFile(),
|
||||||
|
isDirectory: stats.isDirectory()
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// IPC handler for logging audit events
|
// IPC handler for logging audit events
|
||||||
ipcMain.handle('log-audit-event', async (event, { directoryPath, action, details }) => {
|
ipcMain.handle('log-audit-event', async (event, { directoryPath, action, details }) => {
|
||||||
try {
|
try {
|
||||||
@ -906,4 +937,4 @@ app.on('activate', () => {
|
|||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow();
|
createWindow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
53
preload.js
Normal file
53
preload.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const fakeIpcRenderer = {
|
||||||
|
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
|
||||||
|
on: (channel, callback) => {
|
||||||
|
const listener = (_event, ...args) => callback(...args);
|
||||||
|
ipcRenderer.on(channel, listener);
|
||||||
|
return () => ipcRenderer.removeListener(channel, listener);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const fakeElectronModule = {
|
||||||
|
ipcRenderer: fakeIpcRenderer,
|
||||||
|
app: null,
|
||||||
|
BrowserWindow: null,
|
||||||
|
dialog: null,
|
||||||
|
ipcMain: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fakePathModule = {
|
||||||
|
join: (...args) => path.join(...args),
|
||||||
|
dirname: (p) => path.dirname(p),
|
||||||
|
basename: (p) => path.basename(p),
|
||||||
|
extname: (p) => path.extname(p),
|
||||||
|
sep: path.sep,
|
||||||
|
resolve: (...args) => path.resolve(...args),
|
||||||
|
};
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
ipcRenderer: fakeIpcRenderer,
|
||||||
|
});
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('createModuleRequire', () => {
|
||||||
|
return function requirePolyfill(moduleName) {
|
||||||
|
if (moduleName === 'electron') {
|
||||||
|
return fakeElectronModule;
|
||||||
|
}
|
||||||
|
if (moduleName === 'path') {
|
||||||
|
return fakePathModule;
|
||||||
|
}
|
||||||
|
if (moduleName === 'fs') {
|
||||||
|
throw new Error('Direct fs access disabled. Use IPC.');
|
||||||
|
}
|
||||||
|
if (moduleName.startsWith('./')) {
|
||||||
|
const baseName = moduleName.replace('./', '').replace('.js', '');
|
||||||
|
const mod = window[baseName];
|
||||||
|
if (mod) return mod;
|
||||||
|
throw new Error(`Module not found: ${moduleName}`);
|
||||||
|
}
|
||||||
|
throw new Error(`Module not allowed: ${moduleName}`);
|
||||||
|
};
|
||||||
|
});
|
||||||
13
renderer.js
13
renderer.js
@ -1,4 +1,13 @@
|
|||||||
const UIManager = require('./utils/renderer/UIManager');
|
// UIManager is loaded via window.UIManager from index.html
|
||||||
|
const UIManager = window.UIManager;
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.escapeHtml = escapeHtml;
|
||||||
|
|
||||||
// Initialize the application when DOM is loaded
|
// Initialize the application when DOM is loaded
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@ -47,4 +56,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
console.log('UIManager initialized and exposed to window');
|
console.log('UIManager initialized and exposed to window');
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Movie Mapper renderer loaded');
|
console.log('Movie Mapper renderer loaded');
|
||||||
|
|||||||
@ -60,24 +60,37 @@ class FileListManager {
|
|||||||
_createFolderElement(file) {
|
_createFolderElement(file) {
|
||||||
const fileItem = document.createElement('div');
|
const fileItem = document.createElement('div');
|
||||||
fileItem.className = 'file-item folder-item';
|
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
|
const icon = document.createElement('div');
|
||||||
|
icon.style.cssText = 'font-size: 18px; margin-right: 10px;';
|
||||||
|
icon.textContent = '\uD83D\uDCC1';
|
||||||
|
|
||||||
|
const nameEl = document.createElement('div');
|
||||||
|
nameEl.className = 'file-name folder-name';
|
||||||
|
nameEl.textContent = file.name;
|
||||||
|
|
||||||
|
const durationEl = document.createElement('div');
|
||||||
|
durationEl.className = 'file-duration';
|
||||||
|
|
||||||
|
const qualityEl = document.createElement('div');
|
||||||
|
qualityEl.className = 'file-quality';
|
||||||
|
|
||||||
|
const fpsEl = document.createElement('div');
|
||||||
|
fpsEl.className = 'file-fps';
|
||||||
|
|
||||||
|
const tagsEl = document.createElement('div');
|
||||||
|
tagsEl.className = 'file-tags';
|
||||||
|
|
||||||
|
fileItem.append(icon, nameEl, durationEl, qualityEl, fpsEl, tagsEl);
|
||||||
|
|
||||||
|
const filePath = file.path;
|
||||||
|
const fileName = file.name;
|
||||||
fileItem.addEventListener('click', (e) => {
|
fileItem.addEventListener('click', (e) => {
|
||||||
// Don't trigger if clicking on any child elements
|
|
||||||
if (e.target !== fileItem && e.target.className !== 'file-name folder-name') {
|
if (e.target !== fileItem && e.target.className !== 'file-name folder-name') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.onFolderClick) {
|
if (this.onFolderClick) {
|
||||||
this.onFolderClick(file.path, file.name);
|
this.onFolderClick(filePath, fileName);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -97,35 +110,91 @@ class FileListManager {
|
|||||||
fileItem.dataset.index = index;
|
fileItem.dataset.index = index;
|
||||||
fileItem.dataset.filePath = file.path;
|
fileItem.dataset.filePath = file.path;
|
||||||
|
|
||||||
// Use actual duration from file metadata
|
|
||||||
const duration = file.duration || '00:00';
|
const duration = file.duration || '00:00';
|
||||||
const quality = file.quality || 'unknown';
|
const quality = file.quality || 'unknown';
|
||||||
const fps = file.fps || 'unknown';
|
const fps = file.fps || 'unknown';
|
||||||
|
|
||||||
// Add label for problematic files
|
const dragHandle = document.createElement('div');
|
||||||
const problemLabel = file.isProblematic ? '<span class="problematic-label">⚠️</span> ' : '';
|
dragHandle.className = 'drag-handle';
|
||||||
|
dragHandle.textContent = '\u22EE\u22EE';
|
||||||
|
|
||||||
fileItem.innerHTML = `
|
const episodeContainer = document.createElement('div');
|
||||||
<div class="drag-handle">⋮⋮</div>
|
episodeContainer.className = 'episode-number-container';
|
||||||
<div class="episode-number-container">
|
|
||||||
<button class="episode-arrow episode-arrow-left" data-file-path="${file.path}" title="Decrease episode range">◀</button>
|
const leftArrow = document.createElement('button');
|
||||||
<div class="episode-number" data-episode-start="${episodeNumber}" data-episode-end="${episodeNumber}">${episodeNumber}</div>
|
leftArrow.className = 'episode-arrow episode-arrow-left';
|
||||||
<button class="episode-arrow episode-arrow-right" data-file-path="${file.path}" title="Increase episode range">▶</button>
|
leftArrow.title = 'Decrease episode range';
|
||||||
</div>
|
leftArrow.textContent = '\u25C0';
|
||||||
<div class="file-name" data-file-path="${file.path}">${file.name}</div>
|
|
||||||
<div class="file-duration">${duration}</div>
|
const episodeNum = document.createElement('div');
|
||||||
<div class="file-quality">${quality}</div>
|
episodeNum.className = 'episode-number';
|
||||||
<div class="file-fps">${fps}</div>
|
episodeNum.dataset.episodeStart = episodeNumber;
|
||||||
<div class="file-tags">
|
episodeNum.dataset.episodeEnd = episodeNumber;
|
||||||
<span class="tag-icon extra-tag" data-file-path="${file.path}" title="Mark as Extra">🏷️</span>
|
episodeNum.textContent = episodeNumber;
|
||||||
<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>
|
const rightArrow = document.createElement('button');
|
||||||
<span class="video-preview-btn" data-file-path="${file.path}" title="Preview Video">🎬</span>
|
rightArrow.className = 'episode-arrow episode-arrow-right';
|
||||||
<button class="play-button" style="opacity: 0.3; cursor: default; flex-shrink: 0;" data-file-path="${file.path}" disabled>▶️</button>
|
rightArrow.title = 'Increase episode range';
|
||||||
</div>
|
rightArrow.textContent = '\u25B6';
|
||||||
`;
|
|
||||||
|
episodeContainer.append(leftArrow, episodeNum, rightArrow);
|
||||||
|
|
||||||
|
const nameEl = document.createElement('div');
|
||||||
|
nameEl.className = 'file-name';
|
||||||
|
nameEl.textContent = file.name;
|
||||||
|
|
||||||
|
if (file.isProblematic) {
|
||||||
|
const warn = document.createElement('span');
|
||||||
|
warn.className = 'problematic-label';
|
||||||
|
warn.textContent = '\u26A0\ufe0f';
|
||||||
|
nameEl.prepend(warn, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const durEl = document.createElement('div');
|
||||||
|
durEl.className = 'file-duration';
|
||||||
|
durEl.textContent = duration;
|
||||||
|
|
||||||
|
const qualEl = document.createElement('div');
|
||||||
|
qualEl.className = 'file-quality';
|
||||||
|
qualEl.textContent = quality;
|
||||||
|
|
||||||
|
const fpsEl = document.createElement('div');
|
||||||
|
fpsEl.className = 'file-fps';
|
||||||
|
fpsEl.textContent = fps;
|
||||||
|
|
||||||
|
const tagsEl = document.createElement('div');
|
||||||
|
tagsEl.className = 'file-tags';
|
||||||
|
|
||||||
|
const extraTag = document.createElement('span');
|
||||||
|
extraTag.className = 'tag-icon extra-tag';
|
||||||
|
extraTag.title = 'Mark as Extra';
|
||||||
|
extraTag.textContent = '\uD83C\uDFF7\uFE0F';
|
||||||
|
|
||||||
|
const btsTag = document.createElement('span');
|
||||||
|
btsTag.className = 'tag-icon behind-the-scenes-tag';
|
||||||
|
btsTag.title = 'Add Behind the Scenes';
|
||||||
|
btsTag.textContent = '\uD83C\uDFA5';
|
||||||
|
|
||||||
|
const delTag = document.createElement('span');
|
||||||
|
delTag.className = 'tag-icon delete-tag';
|
||||||
|
delTag.title = 'Mark for Deletion';
|
||||||
|
delTag.textContent = '\uD83D\uDDD1\uFE0F';
|
||||||
|
|
||||||
|
const previewBtn = document.createElement('span');
|
||||||
|
previewBtn.className = 'video-preview-btn';
|
||||||
|
previewBtn.title = 'Preview Video';
|
||||||
|
previewBtn.textContent = '\uD83C\uDFAC';
|
||||||
|
|
||||||
|
const playBtn = document.createElement('button');
|
||||||
|
playBtn.className = 'play-button';
|
||||||
|
playBtn.style.cssText = 'opacity: 0.3; cursor: default; flex-shrink: 0;';
|
||||||
|
playBtn.disabled = true;
|
||||||
|
playBtn.textContent = '\u25B6\uFE0F';
|
||||||
|
|
||||||
|
tagsEl.append(extraTag, btsTag, delTag, previewBtn, playBtn);
|
||||||
|
|
||||||
|
fileItem.append(dragHandle, episodeContainer, nameEl, durEl, qualEl, fpsEl, tagsEl);
|
||||||
|
|
||||||
// Add drag and drop event listeners
|
|
||||||
fileItem.addEventListener('dragstart', this._handleDragStart.bind(this));
|
fileItem.addEventListener('dragstart', this._handleDragStart.bind(this));
|
||||||
fileItem.addEventListener('dragend', this._handleDragEnd.bind(this));
|
fileItem.addEventListener('dragend', this._handleDragEnd.bind(this));
|
||||||
fileItem.addEventListener('dragover', this._handleDragOver.bind(this));
|
fileItem.addEventListener('dragover', this._handleDragOver.bind(this));
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
const { ipcRenderer } = require('electron');
|
const { ipcRenderer } = window.electronAPI;
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ModalManager - Manages video preview modal
|
* ModalManager - Manages video preview modal
|
||||||
@ -142,50 +141,42 @@ class ModalManager {
|
|||||||
const videoContainer = document.getElementById('video-preview-container');
|
const videoContainer = document.getElementById('video-preview-container');
|
||||||
const loadingIndicator = document.getElementById('video-preview-loading');
|
const loadingIndicator = document.getElementById('video-preview-loading');
|
||||||
|
|
||||||
// Show loading
|
|
||||||
loadingIndicator.style.display = 'block';
|
loadingIndicator.style.display = 'block';
|
||||||
videoContainer.innerHTML = '';
|
videoContainer.textContent = '';
|
||||||
fileInfo.innerHTML = `<strong>Loading:</strong> ${filePath.split('/').pop()}`;
|
const fileName = filePath.split('/').pop();
|
||||||
|
fileInfo.textContent = `Loading: ${fileName}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if file exists
|
const statsResult = await ipcRenderer.invoke('get-file-stats', filePath);
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!statsResult.success) {
|
||||||
throw new Error('File not found');
|
throw new Error('File not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get file stats for info
|
fileInfo.textContent = `File: ${fileName} | Size: ${(statsResult.size / (1024 * 1024)).toFixed(2)} MB | Path: ${filePath}`;
|
||||||
const stats = fs.statSync(filePath);
|
|
||||||
const fileName = filePath.split('/').pop();
|
|
||||||
|
|
||||||
// Update file info
|
|
||||||
fileInfo.innerHTML = `
|
|
||||||
<strong>File:</strong> ${fileName}<br>
|
|
||||||
<strong>Size:</strong> ${(stats.size / (1024 * 1024)).toFixed(2)} MB<br>
|
|
||||||
<strong>Path:</strong> ${filePath}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// For MKV files, open directly in default player
|
|
||||||
const ext = filePath.toLowerCase().split('.').pop();
|
const ext = filePath.toLowerCase().split('.').pop();
|
||||||
if (ext === 'mkv') {
|
if (ext === 'mkv') {
|
||||||
// Close the modal and open in default player directly
|
|
||||||
this.videoPreviewModal.style.display = 'none';
|
this.videoPreviewModal.style.display = 'none';
|
||||||
ipcRenderer.invoke('open-file-in-player', filePath);
|
ipcRenderer.invoke('open-file-in-player', filePath);
|
||||||
} else {
|
} else {
|
||||||
// For other formats, try to create a video player
|
const video = document.createElement('video');
|
||||||
videoContainer.innerHTML = `
|
video.id = 'preview-video';
|
||||||
<video id="preview-video" controls style="width: 100%; max-width: 800px; height: auto; margin: 0 auto; display: block;">
|
video.controls = true;
|
||||||
<source src="${filePath}" type="video/mp4">
|
video.style.cssText = 'width: 100%; max-width: 800px; height: auto; margin: 0 auto; display: block;';
|
||||||
Your browser does not support the video tag.
|
const source = document.createElement('source');
|
||||||
</video>
|
source.src = `file://${filePath}`;
|
||||||
`;
|
source.type = 'video/mp4';
|
||||||
|
video.appendChild(source);
|
||||||
// Hide loading
|
videoContainer.appendChild(video);
|
||||||
loadingIndicator.style.display = 'none';
|
loadingIndicator.style.display = 'none';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading video preview:', error);
|
console.error('Error loading video preview:', error);
|
||||||
fileInfo.innerHTML = `<strong>Error:</strong> Could not load preview for ${filePath}`;
|
fileInfo.textContent = 'Error: Could not load preview';
|
||||||
videoContainer.innerHTML = '<p style="color: #dc3545; text-align: center;">Error loading video preview</p>';
|
const p = document.createElement('p');
|
||||||
|
p.style.cssText = 'color: #dc3545; text-align: center;';
|
||||||
|
p.textContent = 'Error loading video preview';
|
||||||
|
videoContainer.appendChild(p);
|
||||||
loadingIndicator.style.display = 'none';
|
loadingIndicator.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
const { ipcRenderer } = require('electron');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { ipcRenderer } = window.electronAPI;
|
||||||
const AppState = require('./AppState');
|
const AppState = require('./AppState');
|
||||||
const FileListManager = require('./FileListManager');
|
const FileListManager = require('./FileListManager');
|
||||||
const TagManager = require('./TagManager');
|
const TagManager = require('./TagManager');
|
||||||
@ -417,7 +417,10 @@ class UIManager {
|
|||||||
this.searchResultsEl.innerHTML = '';
|
this.searchResultsEl.innerHTML = '';
|
||||||
|
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
this.searchResultsEl.innerHTML = '<p style="color: #888; text-align: center; padding: 10px;">No shows found.</p>';
|
const p = document.createElement('p');
|
||||||
|
p.style.cssText = 'color: #888; text-align: center; padding: 10px;';
|
||||||
|
p.textContent = 'No shows found.';
|
||||||
|
this.searchResultsEl.appendChild(p);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,14 +428,20 @@ class UIManager {
|
|||||||
const resultItem = document.createElement('div');
|
const resultItem = document.createElement('div');
|
||||||
resultItem.className = 'search-result-item';
|
resultItem.className = 'search-result-item';
|
||||||
|
|
||||||
// Create a more detailed display with separate elements
|
|
||||||
const showName = show.seriesName || show.name;
|
const showName = show.seriesName || show.name;
|
||||||
const showYear = show.firstAired ? show.firstAired.split('-')[0] : '';
|
const showYear = show.firstAired ? show.firstAired.split('-')[0] : '';
|
||||||
|
|
||||||
resultItem.innerHTML = `
|
const nameEl = document.createElement('div');
|
||||||
<div class="show-name">${showName}</div>
|
nameEl.className = 'show-name';
|
||||||
${showYear ? `<div class="show-year">${showYear}</div>` : ''}
|
nameEl.textContent = showName;
|
||||||
`;
|
resultItem.appendChild(nameEl);
|
||||||
|
|
||||||
|
if (showYear) {
|
||||||
|
const yearEl = document.createElement('div');
|
||||||
|
yearEl.className = 'show-year';
|
||||||
|
yearEl.textContent = showYear;
|
||||||
|
resultItem.appendChild(yearEl);
|
||||||
|
}
|
||||||
|
|
||||||
resultItem.addEventListener('click', () => this.selectShow(show));
|
resultItem.addEventListener('click', () => this.selectShow(show));
|
||||||
this.searchResultsEl.appendChild(resultItem);
|
this.searchResultsEl.appendChild(resultItem);
|
||||||
@ -1609,60 +1618,74 @@ class UIManager {
|
|||||||
const videoContainer = document.getElementById('video-preview-container');
|
const videoContainer = document.getElementById('video-preview-container');
|
||||||
const loadingIndicator = document.getElementById('video-preview-loading');
|
const loadingIndicator = document.getElementById('video-preview-loading');
|
||||||
|
|
||||||
// Show loading
|
|
||||||
loadingIndicator.style.display = 'block';
|
loadingIndicator.style.display = 'block';
|
||||||
if (videoContainer) videoContainer.innerHTML = '';
|
if (videoContainer) videoContainer.innerHTML = '';
|
||||||
if (fileInfo) fileInfo.innerHTML = `<strong>Loading:</strong> ${filePath.split('/').pop()}`;
|
if (fileInfo) {
|
||||||
|
const fileName = filePath.split('/').pop();
|
||||||
|
const strong = document.createElement('strong');
|
||||||
|
strong.textContent = 'Loading: ';
|
||||||
|
fileInfo.textContent = '';
|
||||||
|
fileInfo.appendChild(strong);
|
||||||
|
fileInfo.appendChild(document.createTextNode(fileName));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if file exists
|
const statsResult = await ipcRenderer.invoke('get-file-stats', filePath);
|
||||||
const fs = require('fs');
|
if (!statsResult.success) {
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
throw new Error('File not found');
|
throw new Error('File not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get file stats for info
|
|
||||||
const stats = fs.statSync(filePath);
|
|
||||||
const fileName = filePath.split('/').pop();
|
const fileName = filePath.split('/').pop();
|
||||||
|
|
||||||
// Update file info
|
|
||||||
if (fileInfo) {
|
if (fileInfo) {
|
||||||
fileInfo.innerHTML = `
|
fileInfo.textContent = '';
|
||||||
<strong>File:</strong> ${fileName}<br>
|
const parts = [
|
||||||
<strong>Size:</strong> ${(stats.size / (1024 * 1024)).toFixed(2)} MB<br>
|
{ bold: 'File: ', text: fileName },
|
||||||
<strong>Path:</strong> ${filePath}
|
{ bold: '\nSize: ', text: `${(statsResult.size / (1024 * 1024)).toFixed(2)} MB` },
|
||||||
`;
|
{ bold: '\nPath: ', text: filePath }
|
||||||
|
];
|
||||||
|
parts.forEach(p => {
|
||||||
|
const strong = document.createElement('strong');
|
||||||
|
strong.textContent = p.bold;
|
||||||
|
fileInfo.appendChild(strong);
|
||||||
|
fileInfo.appendChild(document.createTextNode(p.text));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// For MKV files, open directly in default player
|
|
||||||
const ext = filePath.toLowerCase().split('.').pop();
|
const ext = filePath.toLowerCase().split('.').pop();
|
||||||
if (ext === 'mkv') {
|
if (ext === 'mkv') {
|
||||||
// Close the modal and open in default player directly
|
|
||||||
if (this.videoPreviewModal) {
|
if (this.videoPreviewModal) {
|
||||||
this.videoPreviewModal.style.display = 'none';
|
this.videoPreviewModal.style.display = 'none';
|
||||||
}
|
}
|
||||||
ipcRenderer.invoke('open-file-in-player', filePath);
|
ipcRenderer.invoke('open-file-in-player', filePath);
|
||||||
} else {
|
} else {
|
||||||
// For other formats, try to create a video player
|
|
||||||
if (videoContainer) {
|
if (videoContainer) {
|
||||||
videoContainer.innerHTML = `
|
const video = document.createElement('video');
|
||||||
<video id="preview-video" controls style="width: 100%; max-width: 800px; height: auto; margin: 0 auto; display: block;">
|
video.id = 'preview-video';
|
||||||
<source src="${filePath}" type="video/mp4">
|
video.controls = true;
|
||||||
Your browser does not support the video tag.
|
video.style.cssText = 'width: 100%; max-width: 800px; height: auto; margin: 0 auto; display: block;';
|
||||||
</video>
|
const source = document.createElement('source');
|
||||||
`;
|
source.src = `file://${filePath}`;
|
||||||
|
source.type = 'video/mp4';
|
||||||
|
video.appendChild(source);
|
||||||
|
videoContainer.appendChild(video);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide loading
|
|
||||||
if (loadingIndicator) loadingIndicator.style.display = 'none';
|
if (loadingIndicator) loadingIndicator.style.display = 'none';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading video preview:', error);
|
console.error('Error loading video preview:', error);
|
||||||
if (fileInfo) {
|
if (fileInfo) {
|
||||||
fileInfo.innerHTML = `<strong>Error:</strong> Could not load preview for ${filePath}`;
|
fileInfo.textContent = '';
|
||||||
|
const strong = document.createElement('strong');
|
||||||
|
strong.textContent = 'Error: ';
|
||||||
|
fileInfo.appendChild(strong);
|
||||||
|
fileInfo.appendChild(document.createTextNode('Could not load preview'));
|
||||||
}
|
}
|
||||||
if (videoContainer) {
|
if (videoContainer) {
|
||||||
videoContainer.innerHTML = '<p style="color: #dc3545; text-align: center;">Error loading video preview</p>';
|
const p = document.createElement('p');
|
||||||
|
p.style.cssText = 'color: #dc3545; text-align: center;';
|
||||||
|
p.textContent = 'Error loading video preview';
|
||||||
|
videoContainer.appendChild(p);
|
||||||
}
|
}
|
||||||
if (loadingIndicator) loadingIndicator.style.display = 'none';
|
if (loadingIndicator) loadingIndicator.style.display = 'none';
|
||||||
}
|
}
|
||||||
@ -1698,14 +1721,13 @@ class UIManager {
|
|||||||
async handleFolderClick(folderPath, folderName) {
|
async handleFolderClick(folderPath, folderName) {
|
||||||
console.log('Folder clicked:', folderName, 'at path:', folderPath);
|
console.log('Folder clicked:', folderName, 'at path:', folderPath);
|
||||||
|
|
||||||
// Validate folder path exists
|
// Validate folder path exists via IPC (no direct fs access)
|
||||||
const fs = require('fs');
|
const validation = await ipcRenderer.invoke('validate-folder', folderPath);
|
||||||
if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) {
|
if (!validation.exists || !validation.isDirectory) {
|
||||||
console.error('Invalid folder path:', folderPath);
|
console.error('Invalid folder path:', folderPath);
|
||||||
return; // Don't navigate
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the folder
|
|
||||||
await this.openDirectory(folderPath);
|
await this.openDirectory(folderPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user