training-academy/hypno-gallery.html

1513 lines
61 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hypno Gallery - Immersive Slideshow</title>
<!-- Core Styles -->
<link rel="stylesheet" href="src/styles/styles.css">
<link rel="stylesheet" href="src/styles/styles-dark-edgy.css">
<style>
/* Hypno Gallery Specific Styles */
.hypno-header {
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
margin: 1rem;
border-radius: 15px;
border: 2px solid #667eea;
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
}
.hypno-header h1 {
color: #ffffff;
font-size: 2.5rem;
margin: 0;
text-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
}
.hypno-header .subtitle {
color: #e8e8e8;
font-size: 1.2rem;
margin-top: 0.5rem;
font-style: italic;
}
/* Settings Panel */
.settings-panel {
background: rgba(102, 126, 234, 0.1);
border: 2px solid #667eea;
border-radius: 15px;
padding: 2rem;
margin: 1rem;
backdrop-filter: blur(10px);
}
.settings-section {
margin-bottom: 2rem;
}
.settings-section h3 {
color: #667eea;
margin-bottom: 1rem;
font-size: 1.3rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.setting-group {
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
padding: 1.5rem;
margin: 1rem 0;
}
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
margin: 1rem 0;
flex-wrap: wrap;
gap: 1rem;
}
.setting-label {
color: #e8e8e8;
font-weight: bold;
min-width: 150px;
}
.setting-control {
flex: 1;
min-width: 200px;
}
.setting-control input[type="range"] {
width: 100%;
background: rgba(102, 126, 234, 0.2);
border-radius: 5px;
height: 8px;
outline: none;
}
.setting-control input[type="range"]::-webkit-slider-thumb {
background: #667eea;
width: 20px;
height: 20px;
border-radius: 50%;
cursor: pointer;
-webkit-appearance: none;
}
.setting-control select {
background: rgba(0, 0, 0, 0.5);
color: #ffffff;
border: 1px solid #667eea;
border-radius: 5px;
padding: 0.5rem;
width: 100%;
}
.setting-value {
color: #667eea;
font-weight: bold;
min-width: 80px;
text-align: right;
}
.checkbox-setting {
display: flex;
align-items: center;
gap: 0.5rem;
}
.checkbox-setting input[type="checkbox"] {
transform: scale(1.2);
accent-color: #667eea;
}
/* Library Status */
.library-status {
background: rgba(52, 152, 219, 0.1);
border: 1px solid #3498db;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
font-family: 'Courier New', monospace;
}
.library-status h3 {
color: #3498db;
margin: 0 0 0.5rem 0;
}
/* Control Buttons */
.control-buttons {
display: flex;
justify-content: center;
gap: 2rem;
margin: 2rem;
flex-wrap: wrap;
}
.start-btn {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 25px;
padding: 1rem 2rem;
font-size: 1.2rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
min-width: 200px;
}
.start-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #5a6fd8, #6a42a0);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
transform: translateY(-2px);
}
.start-btn:disabled {
background: #6c757d;
cursor: not-allowed;
box-shadow: none;
}
.back-btn {
background: rgba(52, 73, 94, 0.9);
color: #ecf0f1;
border: 1px solid #34495e;
border-radius: 8px;
padding: 0.5rem 1rem;
text-decoration: none;
font-weight: bold;
transition: all 0.3s ease;
}
.back-btn:hover {
background: rgba(52, 73, 94, 1);
border-color: #3498db;
}
/* Slideshow Container (hidden initially) */
.slideshow-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: #000;
display: none;
justify-content: center;
align-items: center;
z-index: 9999;
}
.slideshow-image {
max-width: 90%;
max-height: 90%;
object-fit: contain;
transition: opacity 0.5s ease-in-out;
}
.slideshow-controls {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 1rem;
background: rgba(0, 0, 0, 0.7);
padding: 1rem;
border-radius: 10px;
}
.slideshow-controls button {
background: #667eea;
color: white;
border: none;
border-radius: 5px;
padding: 0.5rem 1rem;
cursor: pointer;
font-size: 1rem;
}
.slideshow-controls button:hover {
background: #5a6fd8;
}
.slideshow-info {
position: fixed;
top: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 1rem;
border-radius: 10px;
font-family: 'Courier New', monospace;
}
.navigation {
position: fixed;
top: 20px;
left: 20px;
z-index: 1000;
}
/* Progress Bar */
.progress-container {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 4px;
background: rgba(255, 255, 255, 0.2);
display: none;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
width: 0%;
transition: width linear;
}
/* Directory Selection Styles */
.directory-selection-container {
width: 100%;
}
.linked-directories-list {
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 8px;
padding: 0.8rem;
max-height: 200px;
overflow-y: auto;
margin: 0.5rem 0;
}
.directory-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem;
margin: 0.3rem 0;
background: rgba(102, 126, 234, 0.1);
border: 1px solid rgba(102, 126, 234, 0.2);
border-radius: 6px;
transition: all 0.2s ease;
}
.directory-item:hover {
background: rgba(102, 126, 234, 0.2);
border-color: rgba(102, 126, 234, 0.4);
}
.directory-item.selected {
background: rgba(102, 126, 234, 0.3);
border-color: #667eea;
}
.directory-checkbox {
margin-right: 0.5rem;
transform: scale(1.1);
accent-color: #667eea;
}
.directory-path {
flex: 1;
color: #e8e8e8;
font-size: 0.85rem;
word-break: break-all;
margin-right: 0.5rem;
}
.directory-info {
color: #667eea;
font-size: 0.75rem;
text-align: right;
min-width: 60px;
}
.no-directories-message {
text-align: center;
color: #ccc;
font-style: italic;
padding: 1rem;
}
.directory-actions {
display: flex;
align-items: center;
justify-content: space-between;
}
/* Responsive Design */
@media (max-width: 768px) {
.setting-row {
flex-direction: column;
align-items: flex-start;
}
.setting-label {
min-width: auto;
}
.setting-control {
min-width: auto;
width: 100%;
}
.control-buttons {
flex-direction: column;
align-items: center;
}
.directory-item {
flex-direction: column;
align-items: flex-start;
}
.directory-path {
margin: 0.2rem 0;
}
}
</style>
</head>
<body>
<!-- Navigation -->
<div class="navigation">
<a href="index.html" class="back-btn">← Back to Main</a>
</div>
<!-- Main Content -->
<div class="main-content">
<!-- Header -->
<div class="hypno-header">
<h1>🌀 Hypno Gallery</h1>
<div class="subtitle">Immersive Visual Slideshow Experience</div>
</div>
<!-- Library Status -->
<div class="library-status" id="libraryStatus">
<h3>📚 Media Library Status</h3>
<div id="imageLibraryStatus">Initializing image library...</div>
<button onclick="showImageGallery()" id="view-images-btn" style="margin-top: 10px; padding: 8px 16px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; display: none;">
🖼️ View Image Library
</button>
</div>
<!-- Settings Panel -->
<div class="settings-panel">
<!-- Timing Settings -->
<div class="settings-section">
<h3>⏱️ Timing Settings</h3>
<div class="setting-group">
<div class="setting-row">
<span class="setting-label">Timing Mode:</span>
<div class="setting-control">
<select id="timingMode">
<option value="constant">Constant - Fixed interval</option>
<option value="random">Random - Varies between min/max</option>
<option value="wave">Wave - Sine wave variation</option>
</select>
</div>
</div>
<div class="setting-row" id="constantDuration">
<span class="setting-label">Duration:</span>
<div class="setting-control">
<input type="range" id="durationSlider" min="500" max="10000" value="3000" step="100">
</div>
<span class="setting-value" id="durationValue">3.0s</span>
</div>
<div class="setting-row" id="randomRange" style="display: none;">
<span class="setting-label">Random Range:</span>
<div class="setting-control" style="display: flex; gap: 1rem; align-items: center;">
<div style="flex: 1;">
<label style="color: #ccc; font-size: 0.9rem;">Min:</label>
<input type="range" id="minDurationSlider" min="500" max="8000" value="1500" step="100">
<span id="minDurationValue" style="color: #667eea;">1.5s</span>
</div>
<div style="flex: 1;">
<label style="color: #ccc; font-size: 0.9rem;">Max:</label>
<input type="range" id="maxDurationSlider" min="2000" max="12000" value="5000" step="100">
<span id="maxDurationValue" style="color: #667eea;">5.0s</span>
</div>
</div>
</div>
<div class="setting-row" id="waveSettings" style="display: none;">
<span class="setting-label">Wave Settings:</span>
<div class="setting-control" style="display: flex; gap: 1rem; align-items: center;">
<div style="flex: 1;">
<label style="color: #ccc; font-size: 0.9rem;">Min:</label>
<input type="range" id="waveMinSlider" min="500" max="5000" value="1000" step="100">
<span id="waveMinValue" style="color: #667eea;">1.0s</span>
</div>
<div style="flex: 1;">
<label style="color: #ccc; font-size: 0.9rem;">Max:</label>
<input type="range" id="waveMaxSlider" min="2000" max="10000" value="5000" step="100">
<span id="waveMaxValue" style="color: #667eea;">5.0s</span>
</div>
<div style="flex: 1;">
<label style="color: #ccc; font-size: 0.9rem;">Rate:</label>
<input type="range" id="waveRateSlider" min="50" max="200" value="100" step="10">
<span id="waveRateValue" style="color: #667eea;">100</span>
</div>
</div>
</div>
</div>
</div>
<!-- Directory Selection -->
<div class="settings-section">
<h3>📁 Directory Selection</h3>
<div class="setting-group">
<div class="setting-row" style="align-items: flex-start;">
<span class="setting-label">Image Sources:</span>
<div class="setting-control">
<div class="directory-selection-container">
<div class="checkbox-setting" style="margin-bottom: 0.5rem;">
<input type="checkbox" id="includeCapturedPhotos" checked>
<span class="setting-label">📷 Include Captured Photos</span>
</div>
<div id="linkedDirectoriesList" class="linked-directories-list">
<!-- Linked directories will be populated here -->
</div>
<div class="directory-actions" style="margin-top: 0.5rem;">
<button id="refreshDirectories" class="btn btn-small" style="background: rgba(102, 126, 234, 0.8); color: white; border: none; border-radius: 4px; padding: 4px 8px; font-size: 0.8rem;">
🔄 Refresh
</button>
<span id="selectedDirectoryCount" style="color: #667eea; font-size: 0.85rem; margin-left: 0.5rem;">
0 directories selected
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Image Settings -->
<div class="settings-section">
<h3>🖼️ Image Settings</h3>
<div class="setting-group">
<div class="setting-row">
<span class="setting-label">Image Order:</span>
<div class="setting-control">
<select id="imageOrder">
<option value="random">Random - Shuffle images</option>
<option value="sequential">Sequential - Original order</option>
<option value="reverse">Reverse - Reverse order</option>
</select>
</div>
</div>
<div class="setting-row">
<span class="setting-label">Fit Mode:</span>
<div class="setting-control">
<select id="fitMode">
<option value="contain">Contain - Fit entire image</option>
<option value="cover">Cover - Fill screen (crop if needed)</option>
<option value="stretch">Stretch - Fill screen (distort if needed)</option>
</select>
</div>
</div>
<div class="setting-row">
<div class="checkbox-setting">
<input type="checkbox" id="loopPlayback" checked>
<span class="setting-label">Loop Playback</span>
</div>
</div>
<div class="setting-row">
<div class="checkbox-setting">
<input type="checkbox" id="showProgress" checked>
<span class="setting-label">Show Progress Bar</span>
</div>
</div>
</div>
</div>
<!-- Transition Settings -->
<div class="settings-section">
<h3>✨ Transition Settings</h3>
<div class="setting-group">
<div class="setting-row">
<span class="setting-label">Transition Type:</span>
<div class="setting-control">
<select id="transitionType">
<option value="fade">Fade - Cross-fade between images</option>
<option value="none">None - Instant change</option>
</select>
</div>
</div>
<div class="setting-row" id="transitionDurationRow">
<span class="setting-label">Transition Duration:</span>
<div class="setting-control">
<input type="range" id="transitionDuration" min="100" max="2000" value="500" step="50">
</div>
<span class="setting-value" id="transitionDurationValue">0.5s</span>
</div>
</div>
</div>
</div>
<!-- Control Buttons -->
<div class="control-buttons">
<button class="start-btn" id="startSlideshowBtn" onclick="startSlideshow()">
🌀 Start Hypno Gallery
</button>
</div>
</div>
<!-- Slideshow Container (Hidden) -->
<div class="slideshow-container" id="slideshowContainer">
<img class="slideshow-image" id="slideshowImage" alt="Slideshow Image">
<div class="slideshow-info" id="slideshowInfo">
<div>Image: <span id="currentImageIndex">0</span> / <span id="totalImages">0</span></div>
<div>Timer: <span id="remainingTime">0.0s</span></div>
<div>Status: <span id="slideshowStatus">Ready</span></div>
</div>
<div class="slideshow-controls">
<button onclick="pauseSlideshow()" id="pauseBtn">⏸️ Pause</button>
<button onclick="previousImage()" id="prevBtn">⏮️ Previous</button>
<button onclick="nextImage()" id="nextBtn">⏭️ Next</button>
<button onclick="stopSlideshow()" id="stopBtn">⏹️ Stop</button>
</div>
<div class="progress-container" id="progressContainer">
<div class="progress-bar" id="progressBar"></div>
</div>
</div>
<!-- Core Scripts -->
<script>
// Only load Electron-specific scripts if in Electron environment
if (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') {
console.log('🖥️ Electron environment detected - loading desktop scripts');
} else {
console.log('🌐 Browser environment detected - skipping desktop scripts');
}
</script>
<script src="src/utils/desktop-file-manager.js"></script>
<script src="src/features/images/popupImageManager.js"></script>
<script>
// Hypno Gallery Global Variables
let imageLibrary = [];
let currentSettings = {
timingMode: 'constant',
duration: 3000,
minDuration: 1500,
maxDuration: 5000,
waveMin: 1000,
waveMax: 5000,
waveRate: 100,
imageOrder: 'random',
fitMode: 'contain',
loopPlayback: true,
showProgress: true,
transitionType: 'fade',
transitionDuration: 500,
selectedDirectories: [], // Array of selected directory paths
includeCapturedPhotos: true
};
let slideshow = {
images: [],
currentIndex: 0,
isPlaying: false,
isPaused: false,
timer: null,
nextTimeout: null,
startTime: 0,
waveTime: 0
};
// Initialize Hypno Gallery
async function initializeHypnoGallery() {
console.log('🌀 Initializing Hypno Gallery...');
try {
// Initialize directory selection
await initializeDirectorySelection();
// Initialize image library
await initializeImageLibrary();
// Set up event listeners
setupEventListeners();
console.log('✅ Hypno Gallery ready');
} catch (error) {
console.error('❌ Error initializing Hypno Gallery:', error);
}
}
// Initialize Directory Selection
async function initializeDirectorySelection() {
console.log('📁 Initializing directory selection...');
try {
// Load linked directories from localStorage
const linkedDirectories = JSON.parse(localStorage.getItem('linkedImageDirectories') || '[]');
console.log('📂 Found linked directories:', linkedDirectories);
// Load saved directory selection preferences
const savedSettings = JSON.parse(localStorage.getItem('hypnoGallerySettings') || '{}');
if (savedSettings.selectedDirectories && Array.isArray(savedSettings.selectedDirectories)) {
currentSettings.selectedDirectories = savedSettings.selectedDirectories;
} else {
// If no previous selection, default to empty array (user must manually select)
currentSettings.selectedDirectories = [];
}
if (savedSettings.includeCapturedPhotos !== undefined) {
currentSettings.includeCapturedPhotos = savedSettings.includeCapturedPhotos;
document.getElementById('includeCapturedPhotos').checked = savedSettings.includeCapturedPhotos;
}
console.log('💾 Loaded directory preferences:', currentSettings.selectedDirectories);
// Populate directory list
populateDirectoryList(linkedDirectories);
} catch (error) {
console.error('❌ Error initializing directory selection:', error);
}
}
// Populate Directory List
function populateDirectoryList(directories) {
const directoriesList = document.getElementById('linkedDirectoriesList');
if (directories.length === 0) {
directoriesList.innerHTML = '<div class="no-directories-message">No image directories linked. Use Library to add directories.</div>';
updateSelectedDirectoryCount();
return;
}
let html = '';
directories.forEach((directory, index) => {
const dirPath = typeof directory === 'string' ? directory : directory.path;
const dirName = typeof directory === 'string' ? directory : (directory.name || directory.path);
if (!dirPath) return;
const isSelected = currentSettings.selectedDirectories.includes(dirPath);
// Get directory name from path for display
const displayName = dirPath.split('\\').pop() || dirPath.split('/').pop() || dirPath;
html += `
<div class="directory-item ${isSelected ? 'selected' : ''}" data-path="${dirPath}">
<input type="checkbox" class="directory-checkbox"
data-directory="${dirPath}"
${isSelected ? 'checked' : ''}>
<div class="directory-path" title="${dirPath}">
📁 ${displayName}
</div>
<div class="directory-info" id="dir-count-${index}">
Scanning...
</div>
</div>
`;
});
directoriesList.innerHTML = html;
// Add event listeners to checkboxes
directoriesList.querySelectorAll('.directory-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', handleDirectorySelection);
});
// Count images in each directory (async)
directories.forEach(async (directory, index) => {
const dirPath = typeof directory === 'string' ? directory : directory.path;
if (dirPath) {
const count = await countImagesInDirectory(dirPath);
const countElement = document.getElementById(`dir-count-${index}`);
if (countElement) {
countElement.textContent = `${count} images`;
}
}
});
updateSelectedDirectoryCount();
}
// Handle Directory Selection
function handleDirectorySelection(event) {
const directoryPath = event.target.dataset.directory;
const isChecked = event.target.checked;
const directoryItem = event.target.closest('.directory-item');
if (isChecked) {
if (!currentSettings.selectedDirectories.includes(directoryPath)) {
currentSettings.selectedDirectories.push(directoryPath);
}
directoryItem.classList.add('selected');
} else {
currentSettings.selectedDirectories = currentSettings.selectedDirectories.filter(
path => path !== directoryPath
);
directoryItem.classList.remove('selected');
}
updateSelectedDirectoryCount();
saveHypnoGallerySettings();
console.log('📁 Directory selection updated:', currentSettings.selectedDirectories);
}
// Count Images in Directory
async function countImagesInDirectory(directoryPath) {
try {
if (!window.electronAPI || !window.electronAPI.readDirectory) {
return 0;
}
const files = await window.electronAPI.readDirectory(directoryPath);
const imageExtensions = /\.(jpg|jpeg|png|gif|webp|bmp)$/i;
return files.filter(file => {
const fileName = typeof file === 'object' ? file.name : file;
return imageExtensions.test(fileName);
}).length;
} catch (error) {
console.warn(`Warning: Could not count images in ${directoryPath}:`, error);
return 0;
}
}
// Update Selected Directory Count
function updateSelectedDirectoryCount() {
const countElement = document.getElementById('selectedDirectoryCount');
if (countElement) {
const selectedCount = currentSettings.selectedDirectories.length;
countElement.textContent = `${selectedCount} directories selected`;
if (selectedCount === 0) {
countElement.style.color = '#f39c12';
} else {
countElement.style.color = '#667eea';
}
}
}
// Save Hypno Gallery Settings
function saveHypnoGallerySettings() {
try {
localStorage.setItem('hypnoGallerySettings', JSON.stringify({
selectedDirectories: currentSettings.selectedDirectories,
includeCapturedPhotos: currentSettings.includeCapturedPhotos
}));
} catch (error) {
console.warn('Warning: Could not save Hypno Gallery settings:', error);
}
}
// Initialize Image Library
async function initializeImageLibrary() {
try {
console.log('🖼️ Initializing image library...');
// Check if we're in Electron environment
const isElectron = window.electronAPI && (
window.electronAPI.getImageFiles ||
window.electronAPI.readDirectory
);
if (!isElectron) {
console.log('🌐 Electron API not available - checking alternative methods...');
// Try to use unified image library from desktop file manager
if (window.desktopFileManager && window.desktopFileManager.unifiedImageLibrary) {
let unifiedLibrary = window.desktopFileManager.unifiedImageLibrary;
// Filter by selected directories if any are selected
if (currentSettings.selectedDirectories.length > 0) {
unifiedLibrary = unifiedLibrary.filter(image => {
// Check if image path is within any selected directory
return currentSettings.selectedDirectories.some(selectedDir => {
const imagePath = image.path || image.fullPath || '';
const normalizedImagePath = imagePath.replace(/\\/g, '/');
const normalizedSelectedDir = selectedDir.replace(/\\/g, '/');
return normalizedImagePath.startsWith(normalizedSelectedDir);
});
});
console.log(`📁 Filtered unified library to ${unifiedLibrary.length} images from selected directories`);
}
imageLibrary = [...unifiedLibrary];
console.log(`🖼️ Using unified image library: ${imageLibrary.length} images`);
}
if (imageLibrary.length === 0) {
const message = currentSettings.selectedDirectories.length > 0
? '⚠️ No images found in selected directories'
: '⚠️ Image library unavailable - Electron API not accessible';
document.getElementById('imageLibraryStatus').innerHTML =
`<span style="color: #f39c12;">${message}</span>`;
return;
}
} else {
// Get linked image directories from localStorage
const linkedDirectories = JSON.parse(localStorage.getItem('linkedImageDirectories') || '[]');
const linkedIndividualImages = JSON.parse(localStorage.getItem('linkedIndividualImages') || '[]');
console.log('📁 All linked directories:', linkedDirectories);
console.log('📁 Selected directories:', currentSettings.selectedDirectories);
console.log('🖼️ Linked individual images:', linkedIndividualImages);
// Filter directories based on selection (if no selection, use all)
const directoriesToScan = currentSettings.selectedDirectories.length > 0
? linkedDirectories.filter(dir => {
const dirPath = typeof dir === 'string' ? dir : dir.path;
return currentSettings.selectedDirectories.includes(dirPath);
})
: linkedDirectories;
if (directoriesToScan.length === 0 && linkedIndividualImages.length === 0 && !currentSettings.includeCapturedPhotos) {
document.getElementById('imageLibraryStatus').innerHTML =
'<span style="color: #f39c12;">⚠️ No directories selected for slideshow. Select directories in the settings above.</span>';
return;
}
imageLibrary = [];
console.log(`📂 Scanning ${directoriesToScan.length} selected directories for slideshow`);
// Scan each selected directory for images
for (const directoryData of directoriesToScan) {
try {
const directoryPath = typeof directoryData === 'string' ? directoryData : directoryData.path;
if (!directoryPath) {
console.warn('⚠️ Invalid directory data:', directoryData);
continue;
}
console.log(`📂 Scanning selected directory: ${directoryPath}`);
let images = [];
if (window.electronAPI.getImageFiles) {
images = await window.electronAPI.getImageFiles(directoryPath);
} else if (window.electronAPI.readDirectory) {
const allFiles = await window.electronAPI.readDirectory(directoryPath);
images = allFiles.filter(file =>
/\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.name || file.filename)
);
}
console.log(`🖼️ Found ${images.length} images in ${directoryPath}`);
images.forEach(image => {
imageLibrary.push({
name: image.name,
path: image.path,
fullPath: image.path,
size: image.size || 0,
directory: directoryPath,
dateAdded: new Date().toISOString(),
category: 'directory'
});
});
} catch (error) {
console.error(`❌ Error scanning directory ${directoryData}:`, error);
}
}
// Add individual linked images (only if no directory selection is active, or if explicitly included)
if (currentSettings.selectedDirectories.length === 0) {
// If no directories are specifically selected, include individual images
linkedIndividualImages.forEach(imageData => {
imageLibrary.push({
...imageData,
fullPath: imageData.path || imageData.filePath,
category: 'individual'
});
});
console.log(`🖼️ Added ${linkedIndividualImages.length} individual images (no directory filter)`);
} else {
console.log(`🚫 Skipped ${linkedIndividualImages.length} individual images (directory filter active)`);
}
}
// Load captured photos from localStorage if enabled
if (currentSettings.includeCapturedPhotos) {
try {
const capturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
capturedPhotos.forEach(photo => {
if (photo.data && (photo.data.startsWith('data:image/') || photo.imageData)) {
imageLibrary.push({
name: photo.filename || `captured_${photo.timestamp}.jpg`,
path: photo.data || photo.imageData,
fullPath: photo.data || photo.imageData,
isWebcamCapture: true,
timestamp: photo.timestamp,
category: 'captured'
});
}
});
console.log(`🖼️ Added ${capturedPhotos.length} captured photos`);
} catch (error) {
console.warn('⚠️ Failed to load captured photos:', error);
}
} else {
console.log('📷 Captured photos excluded from slideshow');
}
console.log(`🖼️ Total images loaded: ${imageLibrary.length}`);
// Update status display
if (imageLibrary.length > 0) {
let statusMessage = `${imageLibrary.length} images available`;
// Add context about filtering
if (currentSettings.selectedDirectories.length > 0) {
statusMessage += ` (from ${currentSettings.selectedDirectories.length} selected directories)`;
}
if (!currentSettings.includeCapturedPhotos) {
statusMessage += ` (captured photos excluded)`;
}
document.getElementById('imageLibraryStatus').innerHTML =
`<span style="color: #27ae60;">${statusMessage}</span>`;
document.getElementById('view-images-btn').style.display = 'inline-block';
document.getElementById('startSlideshowBtn').disabled = false;
} else {
let errorMessage = '❌ No images found';
if (currentSettings.selectedDirectories.length > 0) {
errorMessage = '❌ No images found in selected directories';
} else if (!currentSettings.includeCapturedPhotos) {
errorMessage = '❌ No images available (captured photos excluded, no directories selected)';
}
document.getElementById('imageLibraryStatus').innerHTML =
`<span style="color: #e74c3c;">${errorMessage}</span>`;
document.getElementById('startSlideshowBtn').disabled = true;
}
} catch (error) {
console.error('❌ Error initializing image library:', error);
document.getElementById('imageLibraryStatus').innerHTML =
'<span style="color: #e74c3c;">❌ Error loading image library</span>';
}
}
// Set up event listeners for settings
function setupEventListeners() {
// Timing mode change
document.getElementById('timingMode').addEventListener('change', (e) => {
updateTimingSettings(e.target.value);
});
// Duration sliders
document.getElementById('durationSlider').addEventListener('input', (e) => {
const value = parseInt(e.target.value);
currentSettings.duration = value;
document.getElementById('durationValue').textContent = (value / 1000).toFixed(1) + 's';
});
document.getElementById('minDurationSlider').addEventListener('input', (e) => {
const value = parseInt(e.target.value);
currentSettings.minDuration = value;
document.getElementById('minDurationValue').textContent = (value / 1000).toFixed(1) + 's';
// Ensure max is greater than min
const maxSlider = document.getElementById('maxDurationSlider');
if (value >= parseInt(maxSlider.value)) {
maxSlider.value = value + 500;
currentSettings.maxDuration = value + 500;
document.getElementById('maxDurationValue').textContent = ((value + 500) / 1000).toFixed(1) + 's';
}
});
document.getElementById('maxDurationSlider').addEventListener('input', (e) => {
const value = parseInt(e.target.value);
const minValue = parseInt(document.getElementById('minDurationSlider').value);
if (value <= minValue) {
e.target.value = minValue + 500;
currentSettings.maxDuration = minValue + 500;
document.getElementById('maxDurationValue').textContent = ((minValue + 500) / 1000).toFixed(1) + 's';
} else {
currentSettings.maxDuration = value;
document.getElementById('maxDurationValue').textContent = (value / 1000).toFixed(1) + 's';
}
});
// Wave settings
document.getElementById('waveMinSlider').addEventListener('input', (e) => {
const value = parseInt(e.target.value);
currentSettings.waveMin = value;
document.getElementById('waveMinValue').textContent = (value / 1000).toFixed(1) + 's';
});
document.getElementById('waveMaxSlider').addEventListener('input', (e) => {
const value = parseInt(e.target.value);
currentSettings.waveMax = value;
document.getElementById('waveMaxValue').textContent = (value / 1000).toFixed(1) + 's';
});
document.getElementById('waveRateSlider').addEventListener('input', (e) => {
const value = parseInt(e.target.value);
currentSettings.waveRate = value;
document.getElementById('waveRateValue').textContent = value;
});
// Transition duration
document.getElementById('transitionDuration').addEventListener('input', (e) => {
const value = parseInt(e.target.value);
currentSettings.transitionDuration = value;
document.getElementById('transitionDurationValue').textContent = (value / 1000).toFixed(1) + 's';
});
// Other settings
document.getElementById('imageOrder').addEventListener('change', (e) => {
currentSettings.imageOrder = e.target.value;
});
document.getElementById('fitMode').addEventListener('change', (e) => {
currentSettings.fitMode = e.target.value;
});
document.getElementById('loopPlayback').addEventListener('change', (e) => {
currentSettings.loopPlayback = e.target.checked;
});
document.getElementById('showProgress').addEventListener('change', (e) => {
currentSettings.showProgress = e.target.checked;
});
document.getElementById('transitionType').addEventListener('change', (e) => {
currentSettings.transitionType = e.target.value;
updateTransitionSettings(e.target.value);
});
// Include captured photos checkbox
document.getElementById('includeCapturedPhotos').addEventListener('change', (e) => {
currentSettings.includeCapturedPhotos = e.target.checked;
saveHypnoGallerySettings();
console.log('📷 Captured photos inclusion:', e.target.checked);
// Refresh image library
setTimeout(() => {
initializeImageLibrary();
}, 100);
});
// Refresh directories button
document.getElementById('refreshDirectories').addEventListener('click', async () => {
console.log('🔄 Refreshing directories...');
await initializeDirectorySelection();
await initializeImageLibrary();
});
// Keyboard controls
document.addEventListener('keydown', handleKeyPress);
}
// Update timing settings display
function updateTimingSettings(mode) {
currentSettings.timingMode = mode;
// Hide all timing-specific controls
document.getElementById('constantDuration').style.display = 'none';
document.getElementById('randomRange').style.display = 'none';
document.getElementById('waveSettings').style.display = 'none';
// Show relevant controls
switch (mode) {
case 'constant':
document.getElementById('constantDuration').style.display = 'flex';
break;
case 'random':
document.getElementById('randomRange').style.display = 'flex';
break;
case 'wave':
document.getElementById('waveSettings').style.display = 'flex';
break;
}
}
// Update transition settings display
function updateTransitionSettings(type) {
const durationRow = document.getElementById('transitionDurationRow');
if (type === 'none') {
durationRow.style.display = 'none';
} else {
durationRow.style.display = 'flex';
}
}
// Start slideshow
function startSlideshow() {
if (imageLibrary.length === 0) {
alert('No images available for slideshow. Please add images to your library first.');
return;
}
console.log('🌀 Starting Hypno Gallery slideshow...');
// Prepare image array
slideshow.images = [...imageLibrary];
// Apply image ordering
switch (currentSettings.imageOrder) {
case 'random':
shuffleArray(slideshow.images);
break;
case 'reverse':
slideshow.images.reverse();
break;
// 'sequential' needs no change
}
// Initialize slideshow state
slideshow.currentIndex = 0;
slideshow.isPlaying = true;
slideshow.isPaused = false;
slideshow.startTime = Date.now();
slideshow.waveTime = 0;
// Show slideshow container
document.querySelector('.main-content').style.display = 'none';
document.getElementById('slideshowContainer').style.display = 'flex';
// Show/hide progress bar
const progressContainer = document.getElementById('progressContainer');
if (currentSettings.showProgress) {
progressContainer.style.display = 'block';
} else {
progressContainer.style.display = 'none';
}
// Update info display
document.getElementById('totalImages').textContent = slideshow.images.length;
document.getElementById('slideshowStatus').textContent = 'Playing';
// Load first image
loadCurrentImage();
// Start timer
scheduleNextImage();
}
// Load current image
function loadCurrentImage() {
if (slideshow.currentIndex >= slideshow.images.length) {
if (currentSettings.loopPlayback) {
slideshow.currentIndex = 0;
} else {
stopSlideshow();
return;
}
}
const currentImage = slideshow.images[slideshow.currentIndex];
const imgElement = document.getElementById('slideshowImage');
console.log(`🖼️ Loading image ${slideshow.currentIndex + 1}: ${currentImage.name}`);
// Update info
document.getElementById('currentImageIndex').textContent = slideshow.currentIndex + 1;
// Set image source
if (currentImage.isWebcamCapture) {
imgElement.src = currentImage.path; // Base64 data URL
} else {
imgElement.src = `file://${currentImage.fullPath}`;
}
// Apply fit mode
switch (currentSettings.fitMode) {
case 'contain':
imgElement.style.objectFit = 'contain';
break;
case 'cover':
imgElement.style.objectFit = 'cover';
imgElement.style.width = '100vw';
imgElement.style.height = '100vh';
imgElement.style.maxWidth = 'none';
imgElement.style.maxHeight = 'none';
break;
case 'stretch':
imgElement.style.objectFit = 'fill';
imgElement.style.width = '100vw';
imgElement.style.height = '100vh';
imgElement.style.maxWidth = 'none';
imgElement.style.maxHeight = 'none';
break;
}
// Apply transition if needed
if (currentSettings.transitionType === 'fade') {
imgElement.style.transition = `opacity ${currentSettings.transitionDuration}ms ease-in-out`;
imgElement.style.opacity = '0';
setTimeout(() => {
imgElement.style.opacity = '1';
}, 50);
} else {
imgElement.style.transition = 'none';
imgElement.style.opacity = '1';
}
}
// Calculate next image timing
function calculateNextTiming() {
switch (currentSettings.timingMode) {
case 'constant':
return currentSettings.duration;
case 'random':
return Math.random() * (currentSettings.maxDuration - currentSettings.minDuration) + currentSettings.minDuration;
case 'wave':
slideshow.waveTime += currentSettings.waveRate / 1000;
const waveValue = (Math.sin(slideshow.waveTime) + 1) / 2; // 0 to 1
return waveValue * (currentSettings.waveMax - currentSettings.waveMin) + currentSettings.waveMin;
default:
return currentSettings.duration;
}
}
// Schedule next image
function scheduleNextImage() {
if (!slideshow.isPlaying) return;
const nextTiming = calculateNextTiming();
console.log(`⏰ Next image in ${(nextTiming / 1000).toFixed(1)}s`);
// Update progress bar
if (currentSettings.showProgress) {
updateProgressBar(nextTiming);
}
// Update countdown timer
updateCountdownTimer(nextTiming);
slideshow.nextTimeout = setTimeout(() => {
if (slideshow.isPlaying && !slideshow.isPaused) {
nextImage();
}
}, nextTiming);
}
// Update progress bar
function updateProgressBar(duration) {
const progressBar = document.getElementById('progressBar');
progressBar.style.width = '0%';
progressBar.style.transition = `width ${duration}ms linear`;
setTimeout(() => {
if (slideshow.isPlaying) {
progressBar.style.width = '100%';
}
}, 50);
}
// Update countdown timer
function updateCountdownTimer(duration) {
const startTime = Date.now();
if (slideshow.timer) {
clearInterval(slideshow.timer);
}
slideshow.timer = setInterval(() => {
if (!slideshow.isPlaying || slideshow.isPaused) {
clearInterval(slideshow.timer);
return;
}
const elapsed = Date.now() - startTime;
const remaining = Math.max(0, duration - elapsed);
document.getElementById('remainingTime').textContent = (remaining / 1000).toFixed(1) + 's';
if (remaining <= 0) {
clearInterval(slideshow.timer);
}
}, 100);
}
// Go to next image
function nextImage() {
if (!slideshow.isPlaying) return;
slideshow.currentIndex++;
loadCurrentImage();
scheduleNextImage();
}
// Go to previous image
function previousImage() {
if (!slideshow.isPlaying) return;
slideshow.currentIndex--;
if (slideshow.currentIndex < 0) {
slideshow.currentIndex = slideshow.images.length - 1;
}
// Cancel current timing
if (slideshow.nextTimeout) {
clearTimeout(slideshow.nextTimeout);
}
loadCurrentImage();
scheduleNextImage();
}
// Pause slideshow
function pauseSlideshow() {
if (slideshow.isPaused) {
// Resume
slideshow.isPaused = false;
document.getElementById('pauseBtn').textContent = '⏸️ Pause';
document.getElementById('slideshowStatus').textContent = 'Playing';
scheduleNextImage();
} else {
// Pause
slideshow.isPaused = true;
document.getElementById('pauseBtn').textContent = '▶️ Resume';
document.getElementById('slideshowStatus').textContent = 'Paused';
if (slideshow.nextTimeout) {
clearTimeout(slideshow.nextTimeout);
}
if (slideshow.timer) {
clearInterval(slideshow.timer);
}
// Stop progress bar
const progressBar = document.getElementById('progressBar');
progressBar.style.transition = 'none';
}
}
// Stop slideshow
function stopSlideshow() {
console.log('⏹️ Stopping slideshow...');
slideshow.isPlaying = false;
slideshow.isPaused = false;
// Clear timers
if (slideshow.nextTimeout) {
clearTimeout(slideshow.nextTimeout);
}
if (slideshow.timer) {
clearInterval(slideshow.timer);
}
// Hide slideshow, show settings
document.getElementById('slideshowContainer').style.display = 'none';
document.querySelector('.main-content').style.display = 'block';
console.log('✅ Slideshow stopped');
}
// Handle keyboard input
function handleKeyPress(event) {
if (!slideshow.isPlaying) return;
switch (event.key) {
case ' ':
case 'p':
event.preventDefault();
pauseSlideshow();
break;
case 'ArrowRight':
case 'n':
event.preventDefault();
nextImage();
break;
case 'ArrowLeft':
case 'b':
event.preventDefault();
previousImage();
break;
case 'Escape':
case 'q':
event.preventDefault();
stopSlideshow();
break;
}
}
// Utility function to shuffle array
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
// Show image gallery (for testing/preview)
function showImageGallery() {
if (imageLibrary.length === 0) {
alert('No images available in the gallery.');
return;
}
// Create gallery popup
const galleryHtml = `
<div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 10000; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; overflow-y: auto; padding: 20px;">
<h2 style="color: white; margin-bottom: 20px;">🖼️ Image Gallery Preview</h2>
<div style="display: flex; flex-wrap: wrap; gap: 15px; max-width: 90%; justify-content: center;">
${imageLibrary.slice(0, 50).map((image, index) => `
<div style="border: 2px solid #667eea; border-radius: 10px; overflow: hidden; width: 200px;">
<img src="${image.isWebcamCapture ? image.path : 'file://' + image.fullPath}" style="width: 100%; height: 150px; object-fit: cover;" alt="Image ${index + 1}">
<div style="background: rgba(102, 126, 234, 0.8); color: white; padding: 5px; text-align: center; font-size: 12px;">
${image.name}
</div>
</div>
`).join('')}
</div>
${imageLibrary.length > 50 ? `<p style="color: #ccc; margin-top: 20px;">Showing first 50 of ${imageLibrary.length} images</p>` : ''}
<button onclick="this.parentElement.remove()" style="margin-top: 20px; padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 16px;">
❌ Close Gallery
</button>
</div>
`;
document.body.insertAdjacentHTML('beforeend', galleryHtml);
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
console.log('🌀 Hypno Gallery DOM loaded');
setTimeout(initializeHypnoGallery, 1000);
});
// Initialize timing settings display
document.addEventListener('DOMContentLoaded', () => {
updateTimingSettings('constant');
updateTransitionSettings('fade');
});
</script>
</body>
</html>