/** * Popup Image Manager - Handles both punishment popups and periodic random image popups * Part of the Annoyance system for consequence enforcement + periodic visual enhancement */ class PopupImageManager { constructor(dataManager) { this.dataManager = dataManager; this.activePopups = []; this.config = null; this.isEnabled = true; // Periodic popup system this.periodicSystem = { isActive: false, interval: null, minInterval: 30000, // 30 seconds maxInterval: 120000, // 2 minutes displayDuration: 5000, // 5 seconds history: [], maxHistorySize: 50 // Increased from 20 to allow more variety }; this.init(); } init() { this.loadConfiguration(); console.log('PopupImageManager initialized with periodic system'); } /** * Start periodic random image popups */ startPeriodicPopups() { if (this.periodicSystem.isActive) { console.log('⚠️ Periodic popup system already active'); return; } this.periodicSystem.isActive = true; this.scheduleNextPeriodicPopup(); console.log('🚀 Periodic image popup system started'); } /** * Stop periodic random image popups */ stopPeriodicPopups() { if (!this.periodicSystem.isActive) { return; } this.periodicSystem.isActive = false; if (this.periodicSystem.interval) { clearTimeout(this.periodicSystem.interval); this.periodicSystem.interval = null; } console.log('🛑 Periodic image popup system stopped'); } /** * Schedule the next periodic popup */ scheduleNextPeriodicPopup() { if (!this.periodicSystem.isActive) { return; } console.log('🔍 Current periodicSystem state:', this.periodicSystem); const { minInterval, maxInterval } = this.periodicSystem; // Validate intervals before calculation if (typeof minInterval !== 'number' || typeof maxInterval !== 'number' || isNaN(minInterval) || isNaN(maxInterval) || minInterval <= 0 || maxInterval <= 0) { console.error('❌ Invalid periodic popup intervals:', { minInterval, maxInterval, minType: typeof minInterval, maxType: typeof maxInterval }); console.log('🔄 Resetting to default intervals'); this.periodicSystem.minInterval = 30000; // 30 seconds this.periodicSystem.maxInterval = 120000; // 2 minutes return this.scheduleNextPeriodicPopup(); } const interval = minInterval > maxInterval ? minInterval : Math.random() * (maxInterval - minInterval) + minInterval; this.periodicSystem.interval = setTimeout(async () => { try { await this.showPeriodicPopup(); } catch (error) { console.error('Error showing periodic popup:', error); } this.scheduleNextPeriodicPopup(); }, interval); console.log(`⏰ Next periodic popup in ${Math.round(interval / 1000)} seconds`); } /** * Show a periodic random popup */ async showPeriodicPopup() { if (!this.periodicSystem.isActive) { return; } // Get the number of images to show based on configuration const imageCount = this.getImageCount(); console.log(`🖼️ Showing ${imageCount} periodic popup images`); const images = await this.getMultipleRandomPeriodicImages(imageCount); if (images.length === 0) { console.log('⚠️ No images available for periodic popup'); return; } this.displayMultiplePeriodicPopups(images); } /** * Get the number of images to show based on configuration */ getImageCount() { const config = this.config || {}; if (config.countMode === 'fixed') { return Math.max(1, config.imageCount || 1); } else if (config.countMode === 'range') { const min = Math.max(1, config.minCount || 1); const max = Math.max(min, config.maxCount || 3); return Math.floor(Math.random() * (max - min + 1)) + min; } // Default to 1 if no configuration return 1; } /** * Get multiple random images for periodic popups */ async getMultipleRandomPeriodicImages(count) { const allImages = await this.getLinkedImages(); if (allImages.length === 0) { return []; } // Filter out disabled images and recent history const disabledImages = this.dataManager.get('disabledImages') || []; const { history } = this.periodicSystem; // Dynamically adjust history size based on available images // Keep history to about 30% of total images, minimum 20, maximum 100 const totalImages = allImages.length; const optimalHistorySize = Math.min(100, Math.max(20, Math.floor(totalImages * 0.3))); if (this.periodicSystem.maxHistorySize !== optimalHistorySize) { this.periodicSystem.maxHistorySize = optimalHistorySize; console.log(`📊 Adjusted history size to ${optimalHistorySize} (30% of ${totalImages} total images)`); } const availableImages = allImages.filter(img => { const imagePath = typeof img === 'string' ? img : img.src; return !disabledImages.includes(imagePath) && !history.includes(imagePath); }); if (availableImages.length === 0) { console.log('⚠️ No available images (all disabled or recently shown)'); console.log('🔄 Clearing history to allow image reuse'); this.periodicSystem.history = []; // Retry with cleared history const retryAvailableImages = allImages.filter(img => { const imagePath = typeof img === 'string' ? img : img.src; return !disabledImages.includes(imagePath); }); if (retryAvailableImages.length === 0) { console.log('❌ No images available even after clearing history'); return []; } // Use the retry images availableImages.length = 0; availableImages.push(...retryAvailableImages); console.log(`✅ Found ${availableImages.length} images after clearing history`); } // Get unique random images const selectedImages = []; const imagesToChooseFrom = [...availableImages]; for (let i = 0; i < Math.min(count, imagesToChooseFrom.length); i++) { const randomIndex = Math.floor(Math.random() * imagesToChooseFrom.length); const selectedImage = imagesToChooseFrom.splice(randomIndex, 1)[0]; selectedImages.push(selectedImage); // Add to history const imagePath = typeof selectedImage === 'string' ? selectedImage : selectedImage.src; this.periodicSystem.history.push(imagePath); // Manage history size if (this.periodicSystem.history.length > this.periodicSystem.maxHistorySize) { this.periodicSystem.history.shift(); } } return selectedImages; } /** * Get random image for periodic popups (from linked directories and individual files) */ async getRandomPeriodicImage() { // Get images from the same sources as the main library const allImages = await this.getLinkedImages(); if (allImages.length === 0) { console.log('⚠️ No linked images available for popups'); return null; } // Filter out disabled images and recent history const disabledImages = this.dataManager.get('disabledImages') || []; const { history } = this.periodicSystem; const availableImages = allImages.filter(img => { const imagePath = typeof img === 'string' ? img : (img.path || img.cachedPath || img.originalName); return !disabledImages.includes(imagePath) && !history.includes(imagePath); }); // If all images are recent, clear history if (availableImages.length === 0) { this.periodicSystem.history = []; const nonDisabledImages = allImages.filter(img => { const imagePath = typeof img === 'string' ? img : (img.cachedPath || img.originalName); return !disabledImages.includes(imagePath); }); if (nonDisabledImages.length === 0) { return null; } const randomIndex = Math.floor(Math.random() * nonDisabledImages.length); return nonDisabledImages[randomIndex]; } const randomIndex = Math.floor(Math.random() * availableImages.length); const selectedImage = availableImages[randomIndex]; // Add to history const imagePath = typeof selectedImage === 'string' ? selectedImage : (selectedImage.cachedPath || selectedImage.originalName); this.periodicSystem.history.push(imagePath); if (this.periodicSystem.history.length > this.periodicSystem.maxHistorySize) { this.periodicSystem.history.shift(); } return selectedImage; } /** * Get all images from linked directories and individual files (same as main library) */ async getLinkedImages() { const allImages = []; try { // Get linked directories let linkedDirs; try { linkedDirs = JSON.parse(localStorage.getItem('linkedImageDirectories') || '[]'); if (!Array.isArray(linkedDirs)) { linkedDirs = []; } } catch (e) { console.log('Error parsing linkedImageDirectories:', e); linkedDirs = []; } // Get individual linked images let linkedIndividualImages; try { linkedIndividualImages = JSON.parse(localStorage.getItem('linkedIndividualImages') || '[]'); if (!Array.isArray(linkedIndividualImages)) { linkedIndividualImages = []; } } catch (e) { console.log('Error parsing linkedIndividualImages:', e); linkedIndividualImages = []; } // Load images from linked directories using Electron API if (window.electronAPI && linkedDirs.length > 0) { const imageExtensions = /\.(jpg|jpeg|png|gif|webp|bmp)$/i; for (const dir of linkedDirs) { try { if (window.electronAPI.readDirectory) { const result = window.electronAPI.readDirectory(dir.path); let files = []; // Handle both sync and async results if (result && typeof result.then === 'function') { try { files = await result; } catch (asyncError) { console.error(`Error loading images from directory ${dir.path}:`, asyncError); continue; } } else if (Array.isArray(result)) { files = result; } else { console.warn(`Unexpected result type from readDirectory for ${dir.path}:`, typeof result); continue; } const imageFiles = files.filter(file => imageExtensions.test(file.name)); imageFiles.forEach(file => { allImages.push({ name: file.name, path: file.path, category: 'directory', directory: dir.name }); }); } } catch (error) { console.error(`Error loading images from directory ${dir.path}:`, error); } } } // Add individual linked images linkedIndividualImages.forEach(image => { allImages.push({ name: image.name || 'Unknown Image', path: image.path, category: 'individual', directory: 'Individual Images' }); }); } catch (error) { console.error('📸 Error loading linked images:', error); } return allImages; } /** * Display multiple periodic popups with positioning */ displayMultiplePeriodicPopups(images) { const config = this.config || {}; const positioning = config.positioning || 'center'; const allowOverlap = config.allowOverlap !== false; console.log(`🎯 Positioning ${images.length} images using '${positioning}' mode`); images.forEach((imageData, index) => { const position = this.calculatePopupPosition(positioning, index, images.length, allowOverlap); this.displayPositionedPeriodicPopup(imageData, position, index); }); } /** * Calculate position for a popup based on positioning mode */ calculatePopupPosition(positioning, index, total, allowOverlap) { const config = this.config || {}; const viewportWidth = config.viewportWidth || window.innerWidth; const viewportHeight = config.viewportHeight || window.innerHeight; // Popup dimensions (approximate) const popupWidth = 400; const popupHeight = 300; let position = { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }; switch (positioning) { case 'random': if (!allowOverlap && total > 1) { // Grid distribution with some randomness for non-overlapping positions const cols = Math.ceil(Math.sqrt(total)); const rows = Math.ceil(total / cols); const col = index % cols; const row = Math.floor(index / cols); // Calculate available space const maxLeft = Math.max(100, viewportWidth - popupWidth - 50); const maxTop = Math.max(100, viewportHeight - popupHeight - 50); const sectionWidth = maxLeft / cols; const sectionHeight = maxTop / rows; // Add some randomness within each section const randomOffsetX = Math.random() * Math.min(100, sectionWidth * 0.3); const randomOffsetY = Math.random() * Math.min(100, sectionHeight * 0.3); const left = Math.max(50, (col * sectionWidth) + randomOffsetX); const top = Math.max(50, (row * sectionHeight) + randomOffsetY); position = { top: `${top}px`, left: `${left}px`, transform: 'none' }; } else { // Completely random positioning (overlapping allowed) const maxLeft = Math.max(50, viewportWidth - popupWidth); const maxTop = Math.max(50, viewportHeight - popupHeight); const randomLeft = 50 + Math.random() * Math.max(0, maxLeft - 50); const randomTop = 50 + Math.random() * Math.max(0, maxTop - 50); position = { top: `${randomTop}px`, left: `${randomLeft}px`, transform: 'none' }; } break; case 'grid': const cols = Math.ceil(Math.sqrt(total)); const rows = Math.ceil(total / cols); const col = index % cols; const row = Math.floor(index / cols); const gridLeft = (viewportWidth / cols) * col + (viewportWidth / cols - popupWidth) / 2; const gridTop = (viewportHeight / rows) * row + (viewportHeight / rows - popupHeight) / 2; position = { top: `${Math.max(0, gridTop)}px`, left: `${Math.max(0, gridLeft)}px`, transform: 'none' }; break; case 'corners': const corners = [ { top: '10%', left: '10%' }, { top: '10%', right: '10%' }, { bottom: '10%', left: '10%' }, { bottom: '10%', right: '10%' } ]; position = corners[index % corners.length] || corners[0]; break; case 'center': default: // Center with offset for multiple images if (total > 1) { if (!allowOverlap) { // Spread them out more when overlapping is disabled const spacing = Math.min(200, (viewportWidth - popupWidth) / total); const offset = (index - (total - 1) / 2) * spacing; position = { top: '50%', left: `calc(50% + ${offset}px)`, transform: 'translate(-50%, -50%)' }; } else { // Smaller offset when overlapping is allowed const offset = (index - (total - 1) / 2) * 50; position = { top: '50%', left: `calc(50% + ${offset}px)`, transform: 'translate(-50%, -50%)' }; } } else { // Single image, center it perfectly position = { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }; } break; } return position; } /** * Display a positioned periodic popup */ displayPositionedPeriodicPopup(imageData, position, index) { const imageSrc = this.getImageSrc(imageData); const imageName = typeof imageData === 'string' ? 'Random Image' : (imageData.originalName || 'Random Image'); const popup = document.createElement('div'); popup.className = 'periodic-popup-positioned'; // Apply positioning with !important to override any CSS conflicts popup.style.cssText = ` position: fixed !important; z-index: ${10000 + index} !important; top: ${position.top} !important; left: ${position.left} !important; ${position.transform ? `transform: ${position.transform} !important;` : ''} `; console.log(`🎯 Applied position for popup ${index + 1}:`, position); popup.innerHTML = `
💫 Random Visual ${index + 1}
Random Popup
`; this.ensurePeriodicStyles(); document.body.appendChild(popup); // Animate in with slight delay for each popup setTimeout(() => { popup.classList.add('periodic-visible'); }, 50 + (index * 100)); // Auto-hide after configured duration setTimeout(() => { this.hidePeriodicPopup(popup); }, this.getDisplayDuration()); } /** * Get display duration based on configuration */ getDisplayDuration() { const config = this.config || {}; if (config.durationMode === 'fixed') { return (config.displayDuration || 5) * 1000; } else if (config.durationMode === 'range') { const min = Math.max(1, config.minDuration || 3); const max = Math.max(min, config.maxDuration || 10); const randomDuration = Math.floor(Math.random() * (max - min + 1)) + min; return randomDuration * 1000; } // Default to 5 seconds return 5000; } /** * Display periodic popup (legacy single popup method) */ displayPeriodicPopup(imageData) { const imageSrc = this.getImageSrc(imageData); const imageName = typeof imageData === 'string' ? 'Random Image' : (imageData.originalName || 'Random Image'); const popup = document.createElement('div'); popup.className = 'periodic-popup-container'; popup.innerHTML = `
💫 Random Visual
Random Popup
`; this.ensurePeriodicStyles(); document.body.appendChild(popup); // Animate in setTimeout(() => { popup.classList.add('periodic-visible'); }, 50); // Auto-hide setTimeout(() => { this.hidePeriodicPopup(popup); }, this.periodicSystem.displayDuration); } /** * Hide periodic popup */ hidePeriodicPopup(popup) { if (!popup || !popup.parentNode) { return; } popup.classList.add('periodic-hiding'); setTimeout(() => { if (popup && popup.parentNode) { popup.parentNode.removeChild(popup); } }, 300); } /** * Ensure periodic popup styles */ ensurePeriodicStyles() { if (document.querySelector('#periodic-popup-styles')) { return; } const styles = document.createElement('style'); styles.id = 'periodic-popup-styles'; styles.textContent = ` .periodic-popup-container { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10001; opacity: 0; transition: opacity 0.3s ease; pointer-events: none; width: auto; height: auto; } .periodic-popup-container.periodic-visible { opacity: 1; pointer-events: all; } .periodic-popup-container.periodic-hiding { opacity: 0; pointer-events: none; } .periodic-popup-backdrop { width: auto; height: auto; background: rgba(0, 0, 0, 0.6); display: flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 8px; } .periodic-popup-positioned { position: fixed; background: rgba(0, 0, 0, 0.8); border-radius: 8px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); overflow: hidden; max-width: 400px; max-height: 500px; opacity: 1; transition: opacity 0.3s ease; } .periodic-popup-positioned.periodic-hiding { opacity: 0; pointer-events: none; } .periodic-popup-content { background: white; border-radius: 12px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); max-width: 70vw; max-height: 70vh; display: flex; flex-direction: column; cursor: default; animation: periodicSlideIn 0.3s ease-out; } @keyframes periodicSlideIn { from { transform: translateY(-50px) scale(0.9); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } } .periodic-popup-header { padding: 12px 18px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px 12px 0 0; } .periodic-popup-title { font-weight: bold; font-size: 14px; } .periodic-popup-close { background: none; border: none; color: white; font-size: 20px; cursor: pointer; padding: 0; width: 25px; height: 25px; border-radius: 50%; transition: background-color 0.2s; } .periodic-popup-close:hover { background-color: rgba(255, 255, 255, 0.2); } .periodic-popup-image-wrapper { padding: 15px; display: flex; justify-content: center; align-items: center; min-height: 150px; } .periodic-popup-image { max-width: 100%; max-height: 50vh; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .periodic-popup-footer { padding: 8px 18px; border-top: 1px solid #eee; text-align: center; color: #666; font-size: 12px; border-radius: 0 0 12px 12px; background-color: #f8f9fa; } @media (max-width: 768px) { .periodic-popup-content { margin: 20px; max-width: calc(100vw - 40px); max-height: calc(100vh - 40px); } } `; document.head.appendChild(styles); } /** * Update periodic popup settings */ updatePeriodicSettings(settings) { console.log('🔧 Received settings:', settings); if (typeof settings.minInterval === 'number' && !isNaN(settings.minInterval) && settings.minInterval > 0) { this.periodicSystem.minInterval = settings.minInterval * 1000; } if (typeof settings.maxInterval === 'number' && !isNaN(settings.maxInterval) && settings.maxInterval > 0) { this.periodicSystem.maxInterval = settings.maxInterval * 1000; } if (typeof settings.displayDuration === 'number' && !isNaN(settings.displayDuration) && settings.displayDuration > 0) { this.periodicSystem.displayDuration = settings.displayDuration * 1000; } // Ensure minInterval is not greater than maxInterval if (this.periodicSystem.minInterval > this.periodicSystem.maxInterval) { this.periodicSystem.minInterval = this.periodicSystem.maxInterval; } console.log('🔧 Periodic popup settings updated:', { minInterval: this.periodicSystem.minInterval / 1000 + 's', maxInterval: this.periodicSystem.maxInterval / 1000 + 's', displayDuration: this.periodicSystem.displayDuration / 1000 + 's' }); } loadConfiguration() { // Get saved config or use defaults const savedConfig = this.dataManager.get('popupImageConfig'); const defaultConfig = gameData.defaultPopupImageConfig || { enabled: true, imageCount: 3, imageCountMode: 'fixed', minCount: 2, maxCount: 5, displayDuration: 8000, durationMode: 'fixed', minDuration: 5000, maxDuration: 15000, positioning: 'random', allowOverlap: true, fadeAnimation: true, blurBackground: true, preventClose: true, showTimer: true, triggerOnSkip: true, intensity: 'medium' }; this.config = { ...defaultConfig, ...(savedConfig || {}) }; } updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; this.dataManager.set('popupImageConfig', this.config); // Update periodic system timing if frequency/duration config is provided if (newConfig.frequency) { console.log('🔧 Updating periodic system from config frequency:', newConfig.frequency); if (typeof newConfig.frequency.min === 'number' && newConfig.frequency.min > 0) { this.periodicSystem.minInterval = newConfig.frequency.min * 1000; // Convert to milliseconds } if (typeof newConfig.frequency.max === 'number' && newConfig.frequency.max > 0) { this.periodicSystem.maxInterval = newConfig.frequency.max * 1000; // Convert to milliseconds } console.log('Updated periodic system frequency:', { min: this.periodicSystem.minInterval / 1000 + 's', max: this.periodicSystem.maxInterval / 1000 + 's' }); } if (newConfig.duration) { console.log('🔧 Updating periodic system from config duration:', newConfig.duration); if (typeof newConfig.duration.min === 'number' && newConfig.duration.min > 0) { this.periodicSystem.displayDuration = newConfig.duration.min * 1000; // Convert to milliseconds } console.log('Updated periodic system duration:', this.periodicSystem.displayDuration / 1000 + 's'); } console.log('Popup image config updated:', newConfig); } getConfig() { return { ...this.config }; } // Main method to trigger punishment popups triggerPunishmentPopups() { if (!this.config.enabled) { console.log('Punishment popups disabled'); return; } // Get consequence images const images = this.getAvailableImages(); if (images.length === 0) { console.log('No consequence images available for punishment popups'); return; } // Determine how many images to show const imageCount = this.calculateImageCount(); // Clear any existing popups first this.clearAllPopups(); // Create background blur if enabled if (this.config.blurBackground) { this.createBackgroundBlur(); } // Generate popup configurations const popupConfigs = this.generatePopupConfigs(imageCount, images); // Create and show popups with smart delays based on popup count const baseDelay = Math.max(100, Math.min(300, 3000 / popupConfigs.length)); // Dynamic delay: faster for more popups popupConfigs.forEach((config, index) => { setTimeout(() => { this.createPopup(config); }, index * baseDelay); }); console.log(`Triggered ${imageCount} punishment popups`); } getAvailableImages() { // Get consequence images from the game's discovery system const discoveredImages = gameData.discoveredConsequenceImages || []; // Add linked images from directories let linkedImages = []; if (window.linkedImages && Array.isArray(window.linkedImages)) { linkedImages = window.linkedImages.map(img => img.path); } // Get custom consequence images const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; let customConsequenceImages = []; if (!Array.isArray(customImages)) { customConsequenceImages = customImages.consequence || []; } // Get disabled images to filter out const disabledImages = this.dataManager.get('disabledImages') || []; // Combine and filter images const allImages = [...discoveredImages, ...linkedImages, ...customConsequenceImages]; const availableImages = allImages.filter(img => { const imagePath = typeof img === 'string' ? img : (img.cachedPath || img.originalName); return !disabledImages.includes(imagePath); }); return availableImages; } calculateImageCount() { switch (this.config.imageCountMode) { case 'random': return Math.floor(Math.random() * 10) + 1; // 1-10 images (increased from 1-5) case 'range': const min = this.config.minCount; const max = this.config.maxCount; return Math.floor(Math.random() * (max - min + 1)) + min; case 'fixed': default: return this.config.imageCount; } } calculateDuration() { switch (this.config.durationMode) { case 'random': return Math.floor(Math.random() * 10000) + 5000; // 5-15 seconds case 'range': const min = this.config.minDuration; const max = this.config.maxDuration; return Math.floor(Math.random() * (max - min + 1)) + min; case 'fixed': default: return this.config.displayDuration; } } generatePopupConfigs(count, images) { const configs = []; const usedImages = []; const usedPositions = []; for (let i = 0; i < count; i++) { // Select a random image (avoid repeats if possible) let selectedImage; let attempts = 0; do { selectedImage = images[Math.floor(Math.random() * images.length)]; attempts++; } while (usedImages.includes(selectedImage) && attempts < 10 && images.length > 1); usedImages.push(selectedImage); // Get image aspect ratio by loading it temporarily const config = { image: selectedImage, position: null, // Will be set after aspect ratio is determined duration: this.calculateDuration(), index: i, id: `punishment-popup-${Date.now()}-${i}`, aspectRatio: 1.33 // Default aspect ratio }; configs.push(config); } return configs; } generatePosition(index, totalCount, usedPositions, imageAspectRatio = 1.33) { const viewport = { width: window.innerWidth, height: window.innerHeight }; // Calculate popup size based on image aspect ratio and config constraints const maxWidth = Math.min( this.config.maxWidth || 500, viewport.width * (this.config.viewportWidthRatio || 0.35) ); const maxHeight = Math.min( this.config.maxHeight || 400, viewport.height * (this.config.viewportHeightRatio || 0.4) ); const minWidth = this.config.minWidth || 200; const minHeight = this.config.minHeight || 150; let popupSize; if (imageAspectRatio > 1) { // Landscape image - constrain by width const width = Math.max(minWidth, Math.min(maxWidth, maxWidth)); const height = Math.max(minHeight, Math.min(width / imageAspectRatio + 40, maxHeight)); // +40 for header popupSize = { width, height }; } else { // Portrait or square image - constrain by height const height = Math.max(minHeight, Math.min(maxHeight, maxHeight)); const width = Math.max(minWidth, Math.min((height - 40) * imageAspectRatio, maxWidth)); // -40 for header popupSize = { width, height }; } switch (this.config.positioning) { case 'cascade': return { left: 100 + (index * 30), top: 100 + (index * 30), width: popupSize.width, height: popupSize.height }; case 'grid': const cols = Math.ceil(Math.sqrt(totalCount)); const row = Math.floor(index / cols); const col = index % cols; return { left: (viewport.width / cols) * col + (viewport.width / cols - popupSize.width) / 2, top: (viewport.height / cols) * row + (viewport.height / cols - popupSize.height) / 2, width: popupSize.width, height: popupSize.height }; case 'center': const offset = (index - Math.floor(totalCount / 2)) * 50; return { left: (viewport.width - popupSize.width) / 2 + offset, top: (viewport.height - popupSize.height) / 2 + offset, width: popupSize.width, height: popupSize.height }; case 'random': default: let position; let attempts = 0; do { position = { left: Math.random() * (viewport.width - popupSize.width), top: Math.random() * (viewport.height - popupSize.height), width: popupSize.width, height: popupSize.height }; attempts++; } while (!this.config.allowOverlap && this.overlapsExisting(position, usedPositions) && attempts < 20); usedPositions.push(position); return position; } } overlapsExisting(newPos, existingPositions) { // Add padding to prevent popups from being too close const padding = 20; return existingPositions.some(pos => { return !(newPos.left > pos.left + pos.width + padding || newPos.left + newPos.width + padding < pos.left || newPos.top > pos.top + pos.height + padding || newPos.top + newPos.height + padding < pos.top); }); } createBackgroundBlur() { const blur = document.createElement('div'); blur.id = 'punishment-popup-blur'; blur.className = 'punishment-popup-blur'; blur.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.3); backdrop-filter: blur(3px); z-index: 9999; pointer-events: none; `; document.body.appendChild(blur); if (this.config.fadeAnimation) { blur.style.opacity = '0'; requestAnimationFrame(() => { blur.style.transition = 'opacity 0.3s ease-in-out'; blur.style.opacity = '1'; }); } } createPopup(config) { // First, load the image to get its aspect ratio const tempImg = new Image(); const imageSrc = this.getImageSrc(config.image); tempImg.onload = () => { // Calculate aspect ratio const aspectRatio = tempImg.width / tempImg.height; config.aspectRatio = aspectRatio; // Generate position with proper aspect ratio const usedPositions = this.activePopups.map(p => p.config.position); config.position = this.generatePosition(config.index, 1, usedPositions, aspectRatio); // Now create the actual popup this.createPopupElement(config); }; tempImg.onerror = () => { // If image fails to load, use default aspect ratio const usedPositions = this.activePopups.map(p => p.config.position); config.position = this.generatePosition(config.index, 1, usedPositions, 1.33); this.createPopupElement(config); }; tempImg.src = imageSrc; } createPopupElement(config) { const popup = document.createElement('div'); popup.id = config.id; popup.className = 'punishment-popup'; popup.dataset.index = config.index; // Style the popup popup.style.cssText = ` position: fixed; left: ${config.position.left}px; top: ${config.position.top}px; width: ${config.position.width}px; height: ${config.position.height}px; z-index: 10000; background: white; border: 3px solid #dc3545; border-radius: 10px; box-shadow: 0 8px 32px rgba(220, 53, 69, 0.4); overflow: hidden; display: flex; flex-direction: column; pointer-events: ${this.config.preventClose ? 'none' : 'auto'}; `; // Create header with timer if (this.config.showTimer || !this.config.preventClose) { const header = document.createElement('div'); header.className = 'punishment-popup-header'; header.style.cssText = ` background: #dc3545; color: white; padding: 8px 12px; font-size: 12px; font-weight: bold; display: flex; justify-content: space-between; align-items: center; `; if (this.config.showTimer) { const timer = document.createElement('span'); timer.className = 'punishment-popup-timer'; timer.textContent = `${Math.ceil(config.duration / 1000)}s`; header.appendChild(timer); } const title = document.createElement('span'); title.textContent = 'Consequence'; header.appendChild(title); popup.appendChild(header); } // Create image container const imageContainer = document.createElement('div'); imageContainer.style.cssText = ` flex: 1; display: flex; align-items: center; justify-content: center; background: #f8f9fa; padding: 10px; `; // Create image element const img = document.createElement('img'); img.style.cssText = ` max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 5px; `; // Set image source const imageSrc = this.getImageSrc(config.image); img.src = imageSrc; img.alt = 'Consequence Image'; imageContainer.appendChild(img); popup.appendChild(imageContainer); // Add to DOM document.body.appendChild(popup); // Add fade-in animation if (this.config.fadeAnimation) { popup.style.opacity = '0'; popup.style.transform = 'scale(0.8)'; popup.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out'; requestAnimationFrame(() => { popup.style.opacity = '1'; popup.style.transform = 'scale(1)'; }); } // Add to active popups list this.activePopups.push({ element: popup, config: config, startTime: Date.now() }); // Start timer countdown if enabled if (this.config.showTimer) { this.startTimer(popup, config.duration); } // Schedule removal setTimeout(() => { this.removePopup(config.id); }, config.duration); } startTimer(popup, duration) { const timerElement = popup.querySelector('.punishment-popup-timer'); if (!timerElement) return; const startTime = Date.now(); const updateTimer = () => { const elapsed = Date.now() - startTime; const remaining = Math.max(0, duration - elapsed); const seconds = Math.ceil(remaining / 1000); timerElement.textContent = `${seconds}s`; if (remaining > 0) { requestAnimationFrame(updateTimer); } }; requestAnimationFrame(updateTimer); } removePopup(popupId) { const popupData = this.activePopups.find(p => p.config.id === popupId); if (!popupData) return; const popup = popupData.element; if (this.config.fadeAnimation) { popup.style.transition = 'opacity 0.3s ease-in-out, transform 0.3s ease-in-out'; popup.style.opacity = '0'; popup.style.transform = 'scale(0.9)'; setTimeout(() => { if (popup.parentNode) { popup.parentNode.removeChild(popup); } }, 300); } else { if (popup.parentNode) { popup.parentNode.removeChild(popup); } } // Remove from active popups this.activePopups = this.activePopups.filter(p => p.config.id !== popupId); // Remove background blur if no more popups if (this.activePopups.length === 0) { this.removeBackgroundBlur(); } } removeBackgroundBlur() { const blur = document.getElementById('punishment-popup-blur'); if (blur) { if (this.config.fadeAnimation) { blur.style.transition = 'opacity 0.3s ease-in-out'; blur.style.opacity = '0'; setTimeout(() => { if (blur.parentNode) { blur.parentNode.removeChild(blur); } }, 300); } else { blur.parentNode.removeChild(blur); } } } clearAllPopups() { // Remove all active popups immediately this.activePopups.forEach(popupData => { if (popupData.element.parentNode) { popupData.element.parentNode.removeChild(popupData.element); } }); this.activePopups = []; this.removeBackgroundBlur(); } getImageSrc(imageData) { // Handle both old path format and new linked image format if (typeof imageData === 'string') { return imageData; } else if (imageData.dataUrl) { return imageData.dataUrl; } else if (imageData.path) { // New linked image format - convert path for Electron if needed if (window.electronAPI && imageData.path.match(/^[A-Za-z]:\\/)) { return `file:///${imageData.path.replace(/\\/g, '/')}`; } return imageData.path; } else { // Fallback to old format return imageData.cachedPath || imageData.originalName || ''; } } // Preview functionality for testing previewPunishmentPopups(count = 1) { const oldConfig = { ...this.config }; this.config.imageCount = count; this.config.imageCountMode = 'fixed'; this.triggerPunishmentPopups(); // Restore config this.config = oldConfig; } // Utility methods isActive() { return this.activePopups.length > 0; } getActiveCount() { return this.activePopups.length; } getStats() { return { active: this.activePopups.length, config: this.getConfig(), availableImages: this.getAvailableImages().length }; } }