/** * Video Library Manager for Porn Cinema * Handles video library display, search, and organization */ class VideoLibrary { constructor(pornCinema) { this.pornCinema = pornCinema; this.videos = []; this.filteredVideos = []; this.currentView = 'grid'; // 'grid' or 'list' this.currentSort = 'name'; this.sortDirection = 'asc'; this.searchQuery = ''; this.selectedVideo = null; this.selectedVideos = new Set(); // For multi-selection this.isSelectMode = false; // Track if we're in multi-select mode this.initializeElements(); this.attachEventListeners(); } initializeElements() { // View toggle buttons this.gridViewBtn = document.getElementById('grid-view-btn'); this.listViewBtn = document.getElementById('list-view-btn'); // Sort controls this.sortSelect = document.getElementById('sort-select'); this.sortDirectionBtn = document.getElementById('sort-direction'); // Search controls this.searchInput = document.getElementById('library-search'); this.refreshBtn = document.getElementById('refresh-library'); // Playlist controls this.createPlaylistBtn = document.getElementById('create-playlist-btn'); this.selectModeBtn = document.getElementById('select-mode-btn'); // Content container this.libraryContent = document.getElementById('library-content'); if (!this.libraryContent) { console.error(`📁 ❌ library-content element not found!`); } } attachEventListeners() { // View toggle this.gridViewBtn.addEventListener('click', () => this.setView('grid')); this.listViewBtn.addEventListener('click', () => this.setView('list')); // Sort controls this.sortSelect.addEventListener('change', (e) => this.setSortBy(e.target.value)); this.sortDirectionBtn.addEventListener('click', () => this.toggleSortDirection()); // Search this.searchInput.addEventListener('input', (e) => this.setSearchQuery(e.target.value)); this.refreshBtn.addEventListener('click', () => this.refreshLibrary()); // Playlist controls this.createPlaylistBtn.addEventListener('click', () => this.createNewPlaylist()); this.selectModeBtn.addEventListener('click', () => this.toggleSelectMode()); } async loadVideoLibrary() { try { // First check for popup video library data (for windows opened from Quick Play) const popupVideoLibrary = localStorage.getItem('popupVideoLibrary'); if (popupVideoLibrary) { console.log('📁 Loading from popup video library...'); const popupVideos = JSON.parse(popupVideoLibrary); if (popupVideos && popupVideos.length > 0) { console.log(`📁 Loading from popup video library: ${popupVideos.length} videos`); // Convert popup library format to VideoLibrary format const allVideos = popupVideos.map(video => ({ name: video.name, path: video.path, size: video.size || 0, duration: video.duration || 0, thumbnail: video.thumbnail || null, resolution: video.resolution || 'Unknown', format: video.format || this.getFormatFromPath(video.path), bitrate: video.bitrate || 0, dateAdded: video.dateAdded || new Date().toISOString(), category: video.source === 'individual' ? 'individual' : 'directory', directory: video.directory || video.source || 'Unknown' })); this.videos = allVideos; console.log(`📁 Popup video library loaded: ${this.videos.length} videos`); if (this.videos.length === 0) { this.displayEmptyLibrary('No videos found. Open from Quick Play to load video library!'); return; } // Apply filters and sort before displaying this.applyFiltersAndSort(); this.displayLibrary(); return; } } // Then try to get from unified video library (new system) const unifiedLibrary = JSON.parse(localStorage.getItem('unifiedVideoLibrary') || '{}'); if (unifiedLibrary.allVideos && unifiedLibrary.allVideos.length > 0) { console.log(`📁 Loading from unified video library: ${unifiedLibrary.allVideos.length} videos`); // Convert unified library format to VideoLibrary format const allVideos = unifiedLibrary.allVideos.map(video => ({ name: video.name, path: video.path, size: video.size || 0, duration: video.duration || 0, thumbnail: video.thumbnail || null, resolution: video.resolution || 'Unknown', format: video.format || this.getFormatFromPath(video.path), bitrate: video.bitrate || 0, dateAdded: video.dateAdded || new Date().toISOString(), category: video.source === 'individual' ? 'individual' : 'directory', directory: video.directory || video.source || 'Unknown' })); console.log(`📁 Total videos loaded: ${allVideos.length}`); console.log(`📁 From directories: ${allVideos.filter(v => v.category === 'directory').length}`); console.log(`📁 Individual files: ${allVideos.filter(v => v.category === 'individual').length}`); // Process video data with enhanced metadata this.videos = allVideos; console.log(`📁 Final total videos loaded: ${this.videos.length}`); if (this.videos.length === 0) { this.displayEmptyLibrary('No videos found. Add video directories or files in the main library first!'); return; } // Apply filters and sort before displaying this.applyFiltersAndSort(); this.displayLibrary(); return; } } catch (e) { console.warn('Error loading unified video library, falling back to legacy system:', e); } // Fallback to legacy system if unified library not available console.log('📁 Falling back to legacy video loading system...'); try { // Use the same video loading logic as the main library interface let linkedDirs; try { linkedDirs = JSON.parse(localStorage.getItem('linkedVideoDirectories') || '[]'); if (!Array.isArray(linkedDirs)) { linkedDirs = []; } } catch (e) { console.log('Error parsing linkedVideoDirectories, resetting to empty array:', e); linkedDirs = []; } let linkedIndividualVideos; try { linkedIndividualVideos = JSON.parse(localStorage.getItem('linkedIndividualVideos') || '[]'); if (!Array.isArray(linkedIndividualVideos)) { linkedIndividualVideos = []; } } catch (e) { console.log('Error parsing linkedIndividualVideos, resetting to empty array:', e); linkedIndividualVideos = []; } const allVideos = []; // Load videos from linked directories using Electron API if (window.electronAPI && linkedDirs.length > 0) { const videoExtensions = /\.(mp4|webm|avi|mov|mkv|wmv|flv|m4v)$/i; for (const dir of linkedDirs) { try { // Use video-specific directory reading for better results let files = []; if (window.electronAPI.readVideoDirectory) { files = await window.electronAPI.readVideoDirectory(dir.path); } else if (window.electronAPI.readVideoDirectoryRecursive) { files = await window.electronAPI.readVideoDirectoryRecursive(dir.path); } else if (window.electronAPI.readDirectory) { const allFiles = await window.electronAPI.readDirectory(dir.path); files = allFiles.filter(file => videoExtensions.test(file.name)); } if (files && files.length > 0) { files.forEach(file => { allVideos.push({ name: file.name, path: file.path, size: file.size || 0, duration: file.duration || 0, format: this.getFormatFromPath(file.path), dateAdded: new Date().toISOString(), category: 'directory', directory: dir.path }); }); } } catch (error) { console.error(`❌ Error loading videos from directory ${dir.path}:`, error); continue; } } } // Add individual linked video files using the same key as main library linkedIndividualVideos.forEach(video => { allVideos.push({ name: video.name || 'Unknown Video', path: video.path, size: video.size || 0, duration: video.duration || 0, format: this.getFormatFromPath(video.path), dateAdded: video.dateAdded || new Date().toISOString(), category: 'individual', directory: 'Individual Videos' }); }); console.log(`📁 Total videos loaded: ${allVideos.length}`); console.log(`📁 From directories: ${allVideos.filter(v => v.category === 'directory').length}`); console.log(`📁 Individual files: ${allVideos.filter(v => v.category === 'individual').length}`); console.log(`📁 Final total videos loaded: ${allVideos.length}`); if (allVideos.length === 0) { console.log('No videos found in current library system'); this.displayEmptyLibrary('No videos found. Add video directories or files in the main library first!'); return; } // Process video data with enhanced metadata this.videos = allVideos.map(video => ({ name: video.name, path: video.path, size: video.size || 0, duration: video.duration || 0, thumbnail: video.thumbnail || null, resolution: video.resolution || 'Unknown', format: video.format || this.getFormatFromPath(video.path), bitrate: video.bitrate || 0, dateAdded: video.dateAdded || new Date().toISOString(), category: video.category || 'unknown', directory: video.directory || 'Unknown', qualities: this.detectVideoQualities(video) })); console.log(`📁 Processed ${this.videos.length} videos for library display`); // IMPORTANT: Save successfully loaded videos to unified storage for other components if (this.videos.length > 0) { try { const unifiedData = { allVideos: this.videos.map(video => ({ name: video.name, path: video.path, size: video.size, duration: video.duration, format: video.format, dateAdded: video.dateAdded, source: video.category === 'individual' ? 'individual' : 'directory', directory: video.directory, thumbnail: video.thumbnail, resolution: video.resolution, bitrate: video.bitrate })), lastUpdated: new Date().toISOString(), source: 'VideoLibrary' }; localStorage.setItem('unifiedVideoLibrary', JSON.stringify(unifiedData)); console.log(`📁 ✅ Saved ${this.videos.length} videos to unified storage for other components`); } catch (error) { console.warn('📁 ⚠️ Failed to save to unified storage:', error); } } // Apply current filters and display this.applyFiltersAndSort(); this.displayLibrary(); // Load durations for videos that don't have them this.loadMissingDurations(); } catch (error) { console.error('Error loading video library:', error); this.displayEmptyLibrary('Error loading video library'); } } detectVideoQualities(video) { // Detect available qualities for a video // This is a placeholder - in a real implementation, you might // check for multiple files with quality indicators in the name const qualities = ['auto']; if (video.resolution) { const resolution = video.resolution.toLowerCase(); if (resolution.includes('1080') || resolution.includes('1920')) { qualities.push('1080p'); } if (resolution.includes('720') || resolution.includes('1280')) { qualities.push('720p'); } if (resolution.includes('480') || resolution.includes('854')) { qualities.push('480p'); } if (resolution.includes('360') || resolution.includes('640')) { qualities.push('360p'); } } else { // Default to common qualities if resolution unknown qualities.push('1080p', '720p', '480p', '360p'); } return qualities; } setView(view) { this.currentView = view; // Update button states this.gridViewBtn.classList.toggle('active', view === 'grid'); this.listViewBtn.classList.toggle('active', view === 'list'); // Re-display library this.displayLibrary(); } setSortBy(sortBy) { this.currentSort = sortBy; this.applyFiltersAndSort(); this.displayLibrary(); } toggleSortDirection() { this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; this.sortDirectionBtn.textContent = this.sortDirection === 'asc' ? '↑' : '↓'; this.applyFiltersAndSort(); this.displayLibrary(); } setSearchQuery(query) { this.searchQuery = query.toLowerCase(); this.applyFiltersAndSort(); this.displayLibrary(); } applyFiltersAndSort() { // Apply search filter this.filteredVideos = this.videos.filter(video => video.name.toLowerCase().includes(this.searchQuery) ); console.log(`📁 Filter applied: "${this.searchQuery}" -> ${this.filteredVideos.length}/${this.videos.length} videos`); // Apply sorting this.filteredVideos.sort((a, b) => { let aValue, bValue; switch (this.currentSort) { case 'name': aValue = a.name.toLowerCase(); bValue = b.name.toLowerCase(); break; case 'date': aValue = new Date(a.dateAdded); bValue = new Date(b.dateAdded); break; case 'duration': aValue = a.duration; bValue = b.duration; break; case 'size': aValue = a.size; bValue = b.size; break; default: aValue = a.name.toLowerCase(); bValue = b.name.toLowerCase(); } if (aValue < bValue) return this.sortDirection === 'asc' ? -1 : 1; if (aValue > bValue) return this.sortDirection === 'asc' ? 1 : -1; return 0; }); } displayLibrary() { console.log(`📁 DisplayLibrary called - Videos: ${this.videos.length}, Filtered: ${this.filteredVideos.length}`); if (this.filteredVideos.length === 0) { console.log('📁 No filtered videos - applying filter to refresh'); this.applyFiltersAndSort(); // Make sure filtering is applied } if (this.filteredVideos.length === 0) { console.log('📁 Still no filtered videos after applying filter'); this.displayEmptyLibrary('No videos match your search'); return; } const containerClass = this.currentView === 'grid' ? 'library-grid' : 'library-list'; this.libraryContent.innerHTML = `
${this.filteredVideos.map(video => this.createVideoElement(video)).join('')}
`; // Attach click events to video elements this.attachVideoEvents(); } createVideoElement(video) { const duration = this.formatDuration(video.duration); const fileSize = this.formatFileSize(video.size); const isSelected = this.selectedVideos.has(video.path); const selectionClass = isSelected ? 'multi-selected' : ''; // Generate or get thumbnail const thumbnailSrc = this.getThumbnailSrc(video); if (this.currentView === 'grid') { return `
${this.isSelectMode ? `
` : ''}
${thumbnailSrc ? `${video.name}` : `
🎬
` }
${duration}
${video.name}
${video.resolution} ${fileSize}
`; } else { return `
${this.isSelectMode ? `
` : ''}
${thumbnailSrc ? `${video.name}` : `
🎬
` }
${video.name}
${video.resolution}
${duration}
${fileSize}
`; } } attachVideoEvents() { // Video card/item click events const videoElements = this.libraryContent.querySelectorAll('.video-card, .video-list-item'); videoElements.forEach(element => { element.addEventListener('click', (e) => { if (e.target.closest('.video-actions')) return; // Don't trigger on action buttons const videoPath = element.dataset.videoPath; if (this.isSelectMode) { this.toggleVideoSelection(videoPath); } else { this.selectVideo(videoPath); } }); }); // Selection checkbox click events const checkboxes = this.libraryContent.querySelectorAll('.video-selection-checkbox'); checkboxes.forEach(checkbox => { checkbox.addEventListener('click', (e) => { e.stopPropagation(); const videoPath = checkbox.closest('[data-video-path]').dataset.videoPath; this.toggleVideoSelection(videoPath); }); }); // Play button events const playButtons = this.libraryContent.querySelectorAll('.play-video'); playButtons.forEach(button => { button.addEventListener('click', (e) => { e.stopPropagation(); const videoPath = button.closest('[data-video-path]').dataset.videoPath; this.playVideo(videoPath); }); }); // Add to playlist button events const playlistButtons = this.libraryContent.querySelectorAll('.add-to-playlist'); playlistButtons.forEach(button => { button.addEventListener('click', async (e) => { e.stopPropagation(); const videoPath = button.closest('[data-video-path]').dataset.videoPath; await this.addToPlaylist(videoPath); }); }); } selectVideo(videoPath) { // Remove previous selection const previousSelected = this.libraryContent.querySelector('.selected'); if (previousSelected) { previousSelected.classList.remove('selected'); } // Add selection to clicked video const videoElement = this.libraryContent.querySelector(`[data-video-path="${videoPath}"]`); if (videoElement) { videoElement.classList.add('selected'); } // Update selected video this.selectedVideo = this.videos.find(video => video.path === videoPath); // Notify porn cinema if (this.pornCinema) { this.pornCinema.selectVideo(this.selectedVideo); } } playVideo(videoPath) { const video = this.videos.find(video => video.path === videoPath); if (video && this.pornCinema) { this.pornCinema.playVideo(video); } } async addToPlaylist(videoPath) { const video = this.videos.find(video => video.path === videoPath); if (video && this.pornCinema) { await this.pornCinema.addToPlaylist(video); // Also track in global stats if available if (window.playerStats) { window.playerStats.onVideoAddedToPlaylist(video); } } } refreshLibrary(clearThumbnailCache = false) { console.log('🔄 Refreshing video library...'); if (clearThumbnailCache) { console.log('🗑️ Clearing thumbnail cache...'); this.clearThumbnailCache(); } this.videos = []; this.filteredVideos = []; this.selectedVideo = null; // Show loading state this.libraryContent.innerHTML = `

Refreshing video library...

${clearThumbnailCache ? '

Clearing thumbnail cache...

' : ''}
`; // Reload library setTimeout(() => { this.loadVideoLibrary(); }, 500); } clearThumbnailCache() { // Find all thumbnail-related localStorage keys const keysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && (key.startsWith('thumbnail_') || key.startsWith('thumbnail_version_') || key.startsWith('thumbnail_failed_'))) { keysToRemove.push(key); } } // Remove thumbnail cache keys keysToRemove.forEach(key => { localStorage.removeItem(key); }); console.log(`🗑️ Cleared ${keysToRemove.length} thumbnail cache entries`); } displayEmptyLibrary(message) { this.libraryContent.innerHTML = `

📁 ${message}

To add videos to your library:

  1. Return to the home screen
  2. Click "🎬 Manage Video"
  3. Upload videos to your library
  4. Return to Porn Cinema to enjoy them!
`; } // Utility functions formatDuration(seconds) { if (!seconds || seconds === 0) return '--:--'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); if (hours > 0) { return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } else { return `${minutes}:${secs.toString().padStart(2, '0')}`; } } formatFileSize(bytes) { if (!bytes || bytes === 0) return '--'; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; } getFormatFromPath(path) { if (!path) return 'mp4'; const extension = path.toLowerCase().split('.').pop(); return extension || 'mp4'; } // Public methods for external access getSelectedVideo() { return this.selectedVideo; } getVideoByPath(path) { return this.videos.find(video => video.path === path); } getAllVideos() { return this.videos; } getFilteredVideos() { return this.filteredVideos; } loadStoredVideos() { console.log('📁 Loading stored videos from localStorage...'); try { // Fallback to localStorage const storedVideos = JSON.parse(localStorage.getItem('videoFiles') || '{}'); console.log('📁 Falling back to stored videos'); const allVideos = Object.values(storedVideos).flat(); if (!allVideos || allVideos.length === 0) { console.warn('📁 No videos found in storage either'); this.displayEmptyLibrary('No videos found. Please add video files to your video directories.'); return; } // Store videos with enriched metadata this.videos = allVideos.map(video => ({ ...video, category: video.category || 'unknown', qualities: this.detectVideoQualities(video), addedDate: video.addedDate || new Date().toISOString() })); console.log(`📁 Loaded ${this.videos.length} videos`); console.log('📁 Sample video object:', this.videos[0]); // Apply current filters and display this.applyFiltersAndSort(); this.displayLibrary(); // Load durations for videos that don't have them this.loadMissingDurations(); } catch (error) { console.error('Error loading stored videos:', error); this.displayEmptyLibrary('Error loading video library'); } } getThumbnailSrc(video) { // Check if video has existing thumbnail if (video.thumbnail) { // Convert thumbnail path for Electron if needed if (window.electronAPI && video.thumbnail.match(/^[A-Za-z]:\\/)) { return `file:///${video.thumbnail.replace(/\\/g, '/')}`; } return video.thumbnail; } // Check if we have a cached thumbnail const cacheKey = `thumbnail_${video.path}`; const cacheVersionKey = `thumbnail_version_${video.path}`; const currentVersion = '2.0'; // Updated version for aspect ratio support const cachedData = localStorage.getItem(cacheKey); const cachedVersion = localStorage.getItem(cacheVersionKey); if (cachedData && cachedVersion === currentVersion) { try { const thumbnailData = JSON.parse(cachedData); return thumbnailData.dataUrl || cachedData; // Support both new and old format } catch (e) { // Old format - just a data URL string return cachedData; } } else if (cachedData) { // Clear old cache localStorage.removeItem(cacheKey); localStorage.removeItem(cacheVersionKey); } // Check if we've already tried and failed to generate a thumbnail const failureKey = `thumbnail_failed_${video.path}`; const lastFailure = localStorage.getItem(failureKey); const currentTime = Date.now(); // If we failed recently (within 1 hour), use fallback immediately if (lastFailure && (currentTime - parseInt(lastFailure)) < 3600000) { console.log(`📸 Using fallback thumbnail for ${video.name} (recent failure)`); const fallbackThumbnail = this.createFallbackThumbnail(video); this.updateThumbnailDisplay(video.path, fallbackThumbnail); return fallbackThumbnail.dataUrl; } // Generate thumbnail asynchronously this.generateThumbnail(video).catch(error => { // Mark this video as failed for thumbnail generation localStorage.setItem(failureKey, currentTime.toString()); console.warn(`📸 Thumbnail generation failed for ${video.name}, marked for fallback`); }); return null; // Will show placeholder initially } async generateThumbnail(video) { try { // Convert video path for Electron if needed let videoSrc = video.path; if (window.electronAPI && video.path.match(/^[A-Za-z]:\\/)) { videoSrc = `file:///${video.path.replace(/\\/g, '/')}`; } // Create temporary video element const tempVideo = document.createElement('video'); tempVideo.crossOrigin = 'anonymous'; tempVideo.preload = 'metadata'; tempVideo.style.display = 'none'; document.body.appendChild(tempVideo); return new Promise((resolve, reject) => { tempVideo.addEventListener('loadedmetadata', () => { // Seek to 10% of video duration for thumbnail tempVideo.currentTime = Math.min(tempVideo.duration * 0.1, 10); }); tempVideo.addEventListener('seeked', () => { try { // Create canvas to capture frame const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); // Get video dimensions const videoWidth = tempVideo.videoWidth; const videoHeight = tempVideo.videoHeight; const aspectRatio = videoWidth / videoHeight; // Set canvas size based on aspect ratio const maxWidth = 320; const maxHeight = 240; // Increased max height for portrait videos let canvasWidth, canvasHeight; if (aspectRatio >= 1) { // Landscape or square video canvasWidth = Math.min(maxWidth, videoWidth); canvasHeight = canvasWidth / aspectRatio; } else { // Portrait video canvasHeight = Math.min(maxHeight, videoHeight); canvasWidth = canvasHeight * aspectRatio; } canvas.width = canvasWidth; canvas.height = canvasHeight; // Draw video frame to canvas maintaining aspect ratio ctx.drawImage(tempVideo, 0, 0, canvasWidth, canvasHeight); // Convert to base64 data URL const thumbnailDataUrl = canvas.toDataURL('image/jpeg', 0.7); // Cache the thumbnail with metadata const cacheKey = `thumbnail_${video.path}`; const cacheVersionKey = `thumbnail_version_${video.path}`; const thumbnailData = { dataUrl: thumbnailDataUrl, width: canvasWidth, height: canvasHeight, aspectRatio: aspectRatio, isPortrait: aspectRatio < 1 }; localStorage.setItem(cacheKey, JSON.stringify(thumbnailData)); localStorage.setItem(cacheVersionKey, '2.0'); // Update the display this.updateThumbnailDisplay(video.path, thumbnailData); // Cleanup document.body.removeChild(tempVideo); resolve(thumbnailDataUrl); } catch (error) { document.body.removeChild(tempVideo); reject(error); } }); tempVideo.addEventListener('error', (e) => { console.warn(`⚠️ Failed to generate thumbnail for ${video.name}:`, e.message); document.body.removeChild(tempVideo); // Create a fallback thumbnail placeholder that looks better const fallbackThumbnail = this.createFallbackThumbnail(video); this.updateThumbnailDisplay(video.path, fallbackThumbnail); reject(new Error(`Failed to load video for thumbnail: ${e.message}`)); }); // Set timeout to avoid hanging setTimeout(() => { if (tempVideo.parentNode) { document.body.removeChild(tempVideo); } reject(new Error('Timeout generating thumbnail')); }, 10000); tempVideo.src = videoSrc; }); } catch (error) { console.warn(`Failed to generate thumbnail for ${video.name}:`, error); } } createFallbackThumbnail(video) { // Create a stylized fallback thumbnail using canvas const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = 320; canvas.height = 180; // Create gradient background const gradient = ctx.createLinearGradient(0, 0, 320, 180); gradient.addColorStop(0, '#1e293b'); gradient.addColorStop(0.5, '#334155'); gradient.addColorStop(1, '#475569'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, 320, 180); // Add play icon ctx.fillStyle = '#e2e8f0'; ctx.font = 'bold 48px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('▶', 160, 90); // Add video name (truncated) ctx.fillStyle = '#cbd5e1'; ctx.font = '14px Arial'; ctx.textAlign = 'center'; const truncatedName = video.name.length > 30 ? video.name.substring(0, 27) + '...' : video.name; ctx.fillText(truncatedName, 160, 140); // Add "Video" label ctx.fillStyle = '#94a3b8'; ctx.font = '12px Arial'; ctx.fillText('Video File', 160, 160); const fallbackDataUrl = canvas.toDataURL('image/png', 0.8); return { dataUrl: fallbackDataUrl, width: 320, height: 180, aspectRatio: 16/9, isPortrait: false, isFallback: true }; } updateThumbnailDisplay(videoPath, thumbnailData) { // Handle both old (string) and new (object) thumbnail data formats const thumbnailSrc = typeof thumbnailData === 'string' ? thumbnailData : thumbnailData.dataUrl; const isPortrait = typeof thumbnailData === 'object' ? thumbnailData.isPortrait : false; // Find all thumbnail elements for this video const thumbnailElements = document.querySelectorAll(`[data-video-path="${videoPath}"]`); thumbnailElements.forEach(element => { if (element.classList.contains('video-thumbnail') || element.classList.contains('list-thumbnail')) { // Replace placeholder with actual thumbnail const placeholderDiv = element.querySelector('.thumbnail-placeholder'); if (placeholderDiv) { const img = document.createElement('img'); img.src = thumbnailSrc; img.alt = 'Video thumbnail'; img.style.opacity = '0'; img.style.transition = 'opacity 0.3s'; // Apply aspect ratio specific styling if (isPortrait) { img.style.width = 'auto'; img.style.height = '100%'; img.style.objectFit = 'contain'; img.style.maxWidth = '100%'; } else { img.style.width = '100%'; img.style.height = '100%'; img.style.objectFit = 'cover'; } img.onload = () => { img.style.opacity = '1'; placeholderDiv.remove(); }; if (element.classList.contains('list-thumbnail')) { img.className = 'list-thumbnail'; } element.appendChild(img); } } }); } async loadMissingDurations() { // Find videos without duration const videosNeedingDuration = this.videos.filter(video => !video.duration || video.duration === 0); if (videosNeedingDuration.length === 0) { return; } console.log(`⏱️ Loading durations for ${videosNeedingDuration.length} videos...`); // Load durations in batches to avoid overwhelming the system const batchSize = 3; for (let i = 0; i < videosNeedingDuration.length; i += batchSize) { const batch = videosNeedingDuration.slice(i, i + batchSize); await Promise.all(batch.map(async (video) => { try { const duration = await this.getVideoDuration(video); video.duration = duration; // Update the display for this specific video this.updateVideoDurationDisplay(video); } catch (error) { console.warn(`Failed to load duration for ${video.name}:`, error); } })); // Small delay between batches if (i + batchSize < videosNeedingDuration.length) { await new Promise(resolve => setTimeout(resolve, 100)); } } } async getVideoDuration(video) { return new Promise((resolve, reject) => { // Create a temporary video element to load metadata const tempVideo = document.createElement('video'); tempVideo.preload = 'metadata'; // Convert path to proper format for Electron let videoSrc = video.path; if (window.electronAPI && video.path.match(/^[A-Za-z]:\\/)) { // Absolute Windows path - convert to file:// URL videoSrc = `file:///${video.path.replace(/\\/g, '/')}`; } tempVideo.addEventListener('loadedmetadata', () => { const duration = tempVideo.duration; tempVideo.remove(); // Clean up resolve(duration); }); tempVideo.addEventListener('error', (e) => { tempVideo.remove(); // Clean up reject(new Error(`Failed to load video metadata: ${e.message}`)); }); // Set timeout to avoid hanging setTimeout(() => { tempVideo.remove(); reject(new Error('Timeout loading video metadata')); }, 5000); tempVideo.src = videoSrc; }); } updateVideoDurationDisplay(video) { // Escape special characters in the path for CSS selector const escapedPath = video.path.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"'); // Find video cards/items for this video and update the duration display const videoElements = document.querySelectorAll(`[data-video-path="${escapedPath}"]`); // If CSS selector fails, try a different approach if (videoElements.length === 0) { const allVideoElements = document.querySelectorAll('[data-video-path]'); const matchingElements = Array.from(allVideoElements).filter(el => el.getAttribute('data-video-path') === video.path ); matchingElements.forEach((element) => { this.updateSingleElementDuration(element, video); }); } else { videoElements.forEach((element) => { this.updateSingleElementDuration(element, video); }); } } updateSingleElementDuration(element, video) { // Check if this is a grid view card or list view item if (element.classList.contains('video-card')) { // Grid view - duration is in .video-duration const durationElement = element.querySelector('.video-duration'); if (durationElement) { const formattedDuration = this.formatDuration(video.duration); durationElement.textContent = formattedDuration; } } else if (element.classList.contains('video-list-item')) { // List view - duration is in the second .list-meta element const metaElements = element.querySelectorAll('.list-meta'); if (metaElements.length >= 2) { const formattedDuration = this.formatDuration(video.duration); metaElements[1].textContent = formattedDuration; // Second meta is duration } } } // ===== PLAYLIST CREATION METHODS ===== toggleSelectMode() { this.isSelectMode = !this.isSelectMode; // Update button appearance this.selectModeBtn.classList.toggle('active', this.isSelectMode); this.selectModeBtn.textContent = this.isSelectMode ? '✓ Exit Select' : '☑️ Select'; // Clear selections when exiting select mode if (!this.isSelectMode) { this.selectedVideos.clear(); } // Re-render library to show/hide checkboxes this.displayLibrary(); // Show create playlist button if videos are selected this.updatePlaylistControls(); } toggleVideoSelection(videoPath) { if (this.selectedVideos.has(videoPath)) { this.selectedVideos.delete(videoPath); } else { this.selectedVideos.add(videoPath); } // Update visual selection const videoElement = this.libraryContent.querySelector(`[data-video-path="${videoPath}"]`); if (videoElement) { videoElement.classList.toggle('multi-selected', this.selectedVideos.has(videoPath)); const checkbox = videoElement.querySelector('.video-selection-checkbox'); if (checkbox) { checkbox.classList.toggle('checked', this.selectedVideos.has(videoPath)); } } this.updatePlaylistControls(); } updatePlaylistControls() { // Update create playlist button text based on selection if (this.selectedVideos.size > 0) { this.createPlaylistBtn.textContent = `📝 Create Playlist (${this.selectedVideos.size})`; this.createPlaylistBtn.classList.add('has-selection'); } else { this.createPlaylistBtn.textContent = '📝 New Playlist'; this.createPlaylistBtn.classList.remove('has-selection'); } } createNewPlaylist() { if (this.selectedVideos.size > 0) { // Create playlist with selected videos this.createPlaylistFromSelection(); } else { // Create empty playlist this.showCreatePlaylistDialog(); } } createPlaylistFromSelection() { const selectedVideoObjects = Array.from(this.selectedVideos).map(path => this.videos.find(video => video.path === path) ).filter(Boolean); this.showCreatePlaylistDialog(selectedVideoObjects); } showCreatePlaylistDialog(preselectedVideos = []) { // Create modal dialog for playlist creation const modal = document.createElement('div'); modal.className = 'playlist-modal'; modal.innerHTML = `

Create New Playlist

${preselectedVideos.length > 0 ? `

Selected Videos (${preselectedVideos.length}):

${preselectedVideos.map(video => `
${video.name}
`).join('')}
` : `

Creating an empty playlist. You can add videos later using the ➕ button.

`}
`; // Add modal styles modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; z-index: 10002; backdrop-filter: blur(5px); `; document.body.appendChild(modal); // Focus on input and select text const input = modal.querySelector('#new-playlist-name'); setTimeout(() => { input.focus(); input.select(); }, 100); // Setup event handlers this.setupCreatePlaylistModalEvents(modal, preselectedVideos); } setupCreatePlaylistModalEvents(modal, preselectedVideos) { const input = modal.querySelector('#new-playlist-name'); const createBtn = modal.querySelector('.btn-create-playlist'); const createSwitchBtn = modal.querySelector('.btn-create-and-switch'); const closeBtn = modal.querySelector('.playlist-modal-close'); let currentVideos = [...preselectedVideos]; // Close modal handlers const closeModal = () => { document.body.removeChild(modal); }; closeBtn.addEventListener('click', closeModal); modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); // Remove video from creation list const removeButtons = modal.querySelectorAll('.btn-remove-from-creation'); removeButtons.forEach(button => { button.addEventListener('click', () => { const path = button.dataset.path; currentVideos = currentVideos.filter(video => video.path !== path); button.closest('.playlist-creation-video-item').remove(); // Update header count const videoCountSpan = modal.querySelector('.preselected-videos h4'); if (videoCountSpan) { videoCountSpan.textContent = `Selected Videos (${currentVideos.length}):`; } }); }); // Create playlist handlers const createPlaylist = async (switchToPlaylist = false) => { const name = input.value.trim(); if (!name) { input.focus(); return; } try { const playlistData = { name: name, created: new Date().toISOString(), videos: currentVideos.map(video => ({ name: video.name, path: video.path, duration: video.duration, size: video.size, type: video.type })), count: currentVideos.length }; // Save to localStorage const savedPlaylists = this.getSavedPlaylists(); // Check for duplicate names const existingIndex = savedPlaylists.findIndex(p => p.name === name); if (existingIndex >= 0) { if (!confirm(`A playlist named "${name}" already exists. Replace it?`)) { return; } savedPlaylists[existingIndex] = playlistData; } else { savedPlaylists.push(playlistData); } localStorage.setItem('pornCinema_savedPlaylists', JSON.stringify(savedPlaylists)); this.showLibraryNotification(`Created playlist: ${name} (${currentVideos.length} videos)`); // Track playlist creation stats if (window.playerStats) { window.playerStats.onPlaylistCreated(playlistData); } // Switch to playlist if requested if (switchToPlaylist && this.pornCinema) { this.pornCinema.loadSavedPlaylist(playlistData); this.showLibraryNotification(`Switched to playlist: ${name}`); } // Clear selection this.selectedVideos.clear(); this.isSelectMode = false; this.selectModeBtn.classList.remove('active'); this.selectModeBtn.textContent = '☑️ Select'; this.updatePlaylistControls(); this.displayLibrary(); console.log('📝 ✅ Playlist created:', name); closeModal(); } catch (error) { console.error('📝 ❌ Error creating playlist:', error); this.showLibraryNotification('Error creating playlist'); } }; createBtn.addEventListener('click', () => createPlaylist(false)); createSwitchBtn.addEventListener('click', () => createPlaylist(true)); // Enter key to create input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); createPlaylist(false); } }); } getSavedPlaylists() { try { const saved = localStorage.getItem('pornCinema_savedPlaylists'); return saved ? JSON.parse(saved) : []; } catch (error) { console.error('📝 ❌ Error getting saved playlists:', error); return []; } } getExistingPlaylistCount() { return this.getSavedPlaylists().length; } showLibraryNotification(message) { // Create a simple notification system const notification = document.createElement('div'); notification.className = 'library-notification'; notification.textContent = message; notification.style.cssText = ` position: fixed; top: 80px; right: 20px; background: rgba(0, 0, 0, 0.8); color: white; padding: 10px 15px; border-radius: 5px; z-index: 10001; font-size: 14px; backdrop-filter: blur(5px); border: 1px solid rgba(255, 255, 255, 0.1); animation: slideInRight 0.3s ease-out, slideOutRight 0.3s ease-in 2.7s; `; document.body.appendChild(notification); // Remove after 3 seconds setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 3000); } }