// GrabZilla 2.1 - Application Entry Point
// Modular architecture with clear separation of concerns
class GrabZillaApp {
constructor() {
this.state = null;
this.eventBus = null;
this.initialized = false;
this.modules = new Map();
}
// Initialize the application
async init() {
try {
console.log('๐ Initializing GrabZilla 2.1...');
// Initialize event bus
this.eventBus = window.eventBus;
if (!this.eventBus) {
throw new Error('EventBus not available');
}
// Initialize application state
this.state = new window.AppState();
if (!this.state) {
throw new Error('AppState not available');
}
// Set up error handling
this.setupErrorHandling();
// Initialize UI components
await this.initializeUI();
// Set up event listeners
this.setupEventListeners();
// Load saved state if available
await this.loadState();
// Ensure save directory exists
await this.ensureSaveDirectoryExists();
// Check binary status and validate
await this.checkAndValidateBinaries();
// Initialize keyboard navigation
this.initializeKeyboardNavigation();
this.initialized = true;
console.log('โ
GrabZilla 2.1 initialized successfully');
// Notify that the app is ready
this.eventBus.emit('app:ready', { app: this });
} catch (error) {
console.error('โ Failed to initialize GrabZilla:', error);
this.handleInitializationError(error);
}
}
// Set up global error handling
setupErrorHandling() {
// Handle unhandled errors
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
this.eventBus.emit('app:error', {
type: 'global',
error: event.error,
filename: event.filename,
lineno: event.lineno
});
});
// Handle unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
this.eventBus.emit('app:error', {
type: 'promise',
error: event.reason
});
});
// Listen for application errors
this.eventBus.on('app:error', (errorData) => {
// Handle errors appropriately
this.displayError(errorData);
});
}
// Initialize UI components
async initializeUI() {
// Update save path display
this.updateSavePathDisplay();
// Initialize dropdown values
this.initializeDropdowns();
// Set up video list
this.initializeVideoList();
// Set up status display
this.updateStatusMessage('Ready to download videos');
}
// Set up main event listeners
setupEventListeners() {
// State change listeners
this.state.on('videoAdded', (data) => this.onVideoAdded(data));
this.state.on('videoRemoved', (data) => this.onVideoRemoved(data));
this.state.on('videoUpdated', (data) => this.onVideoUpdated(data));
this.state.on('videosReordered', (data) => this.onVideosReordered(data));
this.state.on('videosCleared', (data) => this.onVideosCleared(data));
this.state.on('configUpdated', (data) => this.onConfigUpdated(data));
// UI event listeners
this.setupButtonEventListeners();
this.setupInputEventListeners();
this.setupVideoListEventListeners();
}
// Set up button event listeners
setupButtonEventListeners() {
// Add Video button
const addVideoBtn = document.getElementById('addVideoBtn');
if (addVideoBtn) {
addVideoBtn.addEventListener('click', () => this.handleAddVideo());
}
// Import URLs button
const importUrlsBtn = document.getElementById('importUrlsBtn');
if (importUrlsBtn) {
importUrlsBtn.addEventListener('click', () => this.handleImportUrls());
}
// Save Path button
const savePathBtn = document.getElementById('savePathBtn');
if (savePathBtn) {
savePathBtn.addEventListener('click', () => this.handleSelectSavePath());
}
// Cookie File button
const cookieFileBtn = document.getElementById('cookieFileBtn');
if (cookieFileBtn) {
cookieFileBtn.addEventListener('click', () => this.handleSelectCookieFile());
}
// Control panel buttons
const clearListBtn = document.getElementById('clearListBtn');
if (clearListBtn) {
clearListBtn.addEventListener('click', () => this.handleClearList());
}
const downloadVideosBtn = document.getElementById('downloadVideosBtn');
if (downloadVideosBtn) {
downloadVideosBtn.addEventListener('click', () => this.handleDownloadVideos());
}
const cancelDownloadsBtn = document.getElementById('cancelDownloadsBtn');
if (cancelDownloadsBtn) {
cancelDownloadsBtn.addEventListener('click', () => this.handleCancelDownloads());
}
const updateDepsBtn = document.getElementById('updateDepsBtn');
if (updateDepsBtn) {
updateDepsBtn.addEventListener('click', () => this.handleUpdateDependencies());
}
}
// Set up input event listeners
setupInputEventListeners() {
// URL input - no paste handler needed, user clicks "Add Video" button
const urlInput = document.getElementById('urlInput');
if (urlInput) {
// Optional: could add real-time validation feedback here
}
// Configuration inputs
const defaultQuality = document.getElementById('defaultQuality');
if (defaultQuality) {
defaultQuality.addEventListener('change', (e) => {
this.state.updateConfig({ defaultQuality: e.target.value });
});
}
const defaultFormat = document.getElementById('defaultFormat');
if (defaultFormat) {
defaultFormat.addEventListener('change', (e) => {
this.state.updateConfig({ defaultFormat: e.target.value });
});
}
const filenamePattern = document.getElementById('filenamePattern');
if (filenamePattern) {
filenamePattern.addEventListener('change', (e) => {
this.state.updateConfig({ filenamePattern: e.target.value });
});
}
}
// Set up video list event listeners
setupVideoListEventListeners() {
const videoList = document.getElementById('videoList');
if (videoList) {
videoList.addEventListener('click', (e) => this.handleVideoListClick(e));
videoList.addEventListener('change', (e) => this.handleVideoListChange(e));
this.setupDragAndDrop(videoList);
}
}
// Set up drag-and-drop reordering
setupDragAndDrop(videoList) {
let draggedElement = null;
let draggedVideoId = null;
videoList.addEventListener('dragstart', (e) => {
const videoItem = e.target.closest('.video-item');
if (!videoItem) return;
draggedElement = videoItem;
draggedVideoId = videoItem.dataset.videoId;
videoItem.classList.add('opacity-50');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', videoItem.innerHTML);
});
videoList.addEventListener('dragover', (e) => {
e.preventDefault();
const videoItem = e.target.closest('.video-item');
if (!videoItem || videoItem === draggedElement) return;
e.dataTransfer.dropEffect = 'move';
// Visual feedback - show where it will drop
const rect = videoItem.getBoundingClientRect();
const midpoint = rect.top + rect.height / 2;
if (e.clientY < midpoint) {
videoItem.classList.add('border-t-2', 'border-[#155dfc]');
videoItem.classList.remove('border-b-2');
} else {
videoItem.classList.add('border-b-2', 'border-[#155dfc]');
videoItem.classList.remove('border-t-2');
}
});
videoList.addEventListener('dragleave', (e) => {
const videoItem = e.target.closest('.video-item');
if (videoItem) {
videoItem.classList.remove('border-t-2', 'border-b-2', 'border-[#155dfc]');
}
});
videoList.addEventListener('drop', (e) => {
e.preventDefault();
const targetItem = e.target.closest('.video-item');
if (!targetItem || !draggedVideoId) return;
const targetVideoId = targetItem.dataset.videoId;
// Calculate drop position
const rect = targetItem.getBoundingClientRect();
const midpoint = rect.top + rect.height / 2;
const dropBefore = e.clientY < midpoint;
// Reorder in state
this.handleVideoReorder(draggedVideoId, targetVideoId, dropBefore);
// Clean up visual feedback
targetItem.classList.remove('border-t-2', 'border-b-2', 'border-[#155dfc]');
});
videoList.addEventListener('dragend', (e) => {
const videoItem = e.target.closest('.video-item');
if (videoItem) {
videoItem.classList.remove('opacity-50');
}
// Clean up all visual feedback
document.querySelectorAll('.video-item').forEach(item => {
item.classList.remove('border-t-2', 'border-b-2', 'border-[#155dfc]');
});
draggedElement = null;
draggedVideoId = null;
});
}
handleVideoReorder(draggedId, targetId, insertBefore) {
const videos = this.state.getVideos();
const draggedIndex = videos.findIndex(v => v.id === draggedId);
const targetIndex = videos.findIndex(v => v.id === targetId);
if (draggedIndex === -1 || targetIndex === -1) return;
let newIndex = targetIndex;
if (draggedIndex < targetIndex && !insertBefore) {
newIndex = targetIndex;
} else if (draggedIndex > targetIndex && insertBefore) {
newIndex = targetIndex;
} else if (insertBefore) {
newIndex = targetIndex;
} else {
newIndex = targetIndex + 1;
}
this.state.reorderVideos(draggedIndex, newIndex);
}
// Handle clicks in video list (checkboxes, delete buttons)
handleVideoListClick(event) {
const target = event.target;
const videoItem = target.closest('.video-item');
if (!videoItem) return;
const videoId = videoItem.dataset.videoId;
if (!videoId) return;
// Handle checkbox click
if (target.closest('.video-checkbox')) {
event.preventDefault();
this.toggleVideoSelection(videoId);
return;
}
// Handle delete button click (if we add one later)
if (target.closest('.delete-video-btn')) {
event.preventDefault();
this.handleRemoveVideo(videoId);
return;
}
}
// Handle dropdown changes in video list (quality, format)
handleVideoListChange(event) {
const target = event.target;
const videoItem = target.closest('.video-item');
if (!videoItem) return;
const videoId = videoItem.dataset.videoId;
if (!videoId) return;
// Handle quality dropdown change
if (target.classList.contains('quality-select')) {
const quality = target.value;
this.state.updateVideo(videoId, { quality });
console.log(`Updated video ${videoId} quality to ${quality}`);
return;
}
// Handle format dropdown change
if (target.classList.contains('format-select')) {
const format = target.value;
this.state.updateVideo(videoId, { format });
console.log(`Updated video ${videoId} format to ${format}`);
return;
}
}
// Toggle video selection
toggleVideoSelection(videoId) {
this.state.toggleVideoSelection(videoId);
this.updateVideoCheckbox(videoId);
}
// Update checkbox visual state
updateVideoCheckbox(videoId) {
const videoItem = document.querySelector(`[data-video-id="${videoId}"]`);
if (!videoItem) return;
const checkbox = videoItem.querySelector('.video-checkbox');
if (!checkbox) return;
const isSelected = this.state.ui.selectedVideos.includes(videoId);
checkbox.setAttribute('aria-checked', isSelected ? 'true' : 'false');
// Update checkbox SVG
const svg = checkbox.querySelector('svg');
if (svg) {
if (isSelected) {
svg.innerHTML = `Checking...';
}
const versions = await window.IPCManager.checkBinaryVersions();
// Handle both ytDlp (from main.js) and ytdlp (legacy) formats
const ytdlp = versions.ytDlp || versions.ytdlp;
const ffmpeg = versions.ffmpeg;
if (versions && (ytdlp || ffmpeg)) {
// Update both button status and version display
const ytdlpMissing = !ytdlp || !ytdlp.available;
const ffmpegMissing = !ffmpeg || !ffmpeg.available;
if (ytdlpMissing || ffmpegMissing) {
this.updateDependenciesButtonStatus('missing');
this.updateBinaryVersionDisplay(null);
} else {
this.updateDependenciesButtonStatus('ok');
// Normalize the format for display
const normalizedVersions = {
ytdlp: ytdlp,
ffmpeg: ffmpeg
};
this.updateBinaryVersionDisplay(normalizedVersions);
// Show dialog if updates are available
if (ytdlp.updateAvailable) {
this.showInfo({
title: 'Update Available',
message: `A newer version of yt-dlp is available:\nInstalled: ${ytdlp.version}\nLatest: ${ytdlp.latestVersion || 'newer version'}\n\nPlease run 'npm run setup' to update.`
});
}
}
} else {
this.showError('Could not check binary versions');
}
} catch (error) {
console.error('Error checking dependencies:', error);
this.showError(`Failed to check dependencies: ${error.message}`);
} finally {
// Restore button state
if (btn) {
btn.disabled = false;
btn.innerHTML = originalBtnHTML || '
Check for Updates';
}
}
}
// State change handlers
onVideoAdded(data) {
this.renderVideoList();
this.updateStatsDisplay();
}
onVideoRemoved(data) {
this.renderVideoList();
this.updateStatsDisplay();
}
onVideoUpdated(data) {
this.updateVideoElement(data.video);
this.updateStatsDisplay();
}
onVideosReordered(data) {
// Re-render entire list to reflect new order
this.renderVideoList();
console.log('Video order updated:', data);
}
onVideosCleared(data) {
this.renderVideoList();
this.updateStatsDisplay();
}
onConfigUpdated(data) {
this.updateConfigUI(data.config);
}
// UI update methods
updateSavePathDisplay() {
const savePathElement = document.getElementById('savePath');
if (savePathElement) {
savePathElement.textContent = this.state.config.savePath;
}
}
initializeDropdowns() {
// Set dropdown values from config
const defaultQuality = document.getElementById('defaultQuality');
if (defaultQuality) {
defaultQuality.value = this.state.config.defaultQuality;
}
const defaultFormat = document.getElementById('defaultFormat');
if (defaultFormat) {
defaultFormat.value = this.state.config.defaultFormat;
}
const filenamePattern = document.getElementById('filenamePattern');
if (filenamePattern) {
filenamePattern.value = this.state.config.filenamePattern;
}
}
initializeVideoList() {
this.renderVideoList();
}
renderVideoList() {
const videoList = document.getElementById('videoList');
if (!videoList) return;
const videos = this.state.getVideos();
// Clear all existing videos (including mockups)
videoList.innerHTML = '';
// If no videos, show empty state
if (videos.length === 0) {
videoList.innerHTML = `
No videos yet
Paste YouTube or Vimeo URLs above to get started