/**
* 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 ?
`

` :
`
🎬
`
}
${duration}
${video.name}
${video.resolution}
${fileSize}
`;
} else {
return `
${this.isSelectMode ? `
✓
` : ''}
${thumbnailSrc ?
`

` :
`
🎬
`
}
${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:
- Return to the home screen
- Click "🎬 Manage Video"
- Upload videos to your library
- 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 = `
`;
// 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);
}
}