/**
* 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 = `
`;
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 = `
`;
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
};
}
}