training-academy/hypno-grid.html

4021 lines
166 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grid Hypno Slideshow</title>
<!-- Core Styles -->
<link rel="stylesheet" href="src/styles/color-variables.css">
<link rel="stylesheet" href="src/styles/styles.css">
<link rel="stylesheet" href="src/styles/styles-dark-edgy.css">
<script src="src/utils/themeManager.js"></script>
<style>
/* Hypno Gallery Specific Styles */
.hypno-header {
position: relative;
text-align: center;
background: var(--color-gradient);
padding: 2rem;
margin: 1rem;
border-radius: 15px;
border: 2px solid var(--color-primary);
box-shadow: 0 8px 32px var(--color-primary-transparent);
}
.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: var(--color-primary-transparent);
border: 2px solid var(--color-primary);
border-radius: 15px;
padding: 2rem;
margin: 1rem;
backdrop-filter: blur(10px);
}
.settings-section {
margin-bottom: 2rem;
}
.settings-section h3 {
color: var(--color-primary);
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: var(--color-primary-transparent);
border-radius: 5px;
height: 8px;
outline: none;
}
.setting-control input[type="range"]::-webkit-slider-thumb {
background: var(--color-primary);
width: 20px;
height: 20px;
border-radius: 50%;
cursor: pointer;
}
/* Grid Layout Styles */
.grid-mode-selector {
background: var(--color-primary-transparent);
border: 2px solid var(--color-primary);
border-radius: 10px;
padding: 1rem;
margin-bottom: 1rem;
}
.grid-container {
width: 100%;
height: 70vh;
background: #000;
border-radius: 10px;
border: 2px solid var(--color-primary);
position: relative;
overflow: hidden;
}
.grid-container.single {
display: flex;
align-items: center;
justify-content: center;
}
.grid-container.grid-2x2 {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 2px;
}
.grid-container.grid-3x3 {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
gap: 2px;
}
.grid-cell {
position: relative;
background: #111;
border: 1px solid #333;
overflow: hidden;
display: flex;
flex-direction: column;
}
.grid-cell-image {
width: 100%;
height: 100%;
object-fit: contain;
opacity: 0;
transition: opacity 0.3s ease;
}
.grid-cell-image.visible {
opacity: 1;
}
.grid-cell-info {
position: absolute;
top: 5px;
left: 5px;
background: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.7rem;
z-index: 10;
}
.grid-cell-controls {
position: absolute;
top: 5px;
right: 5px;
display: flex;
gap: 2px;
z-index: 10;
}
.grid-cell-btn {
background: var(--color-primary-hover);
color: white;
border: none;
border-radius: 3px;
padding: 2px 4px;
cursor: pointer;
font-size: 0.7rem;
}
.grid-cell-btn:hover {
background: var(--color-primary);
}
.grid-progress-container {
display: none !important;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: rgba(0, 0, 0, 0.5);
z-index: 10;
}
.grid-progress-bar {
height: 100%;
background: var(--color-primary);
width: 0%;
transition: width linear;
}
.grid-controls {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.grid-assignment {
margin-top: 1rem;
padding: 1rem;
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
}
.cell-assignment {
display: flex;
align-items: center;
gap: 1rem;
margin: 0.5rem 0;
}
.cell-assignment label {
min-width: 80px;
color: #ccc;
}
.cell-assignment select {
flex: 1;
padding: 0.25rem;
background: #333;
color: #fff;
border: 1px solid var(--color-primary);
border-radius: 5px;
}
/* Fullscreen Grid Container */
.grid-slideshow-container {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: #000;
z-index: 1000;
flex-direction: column;
}
.grid-slideshow-display {
flex: 1;
width: 100%;
height: 100%;
position: relative;
}
.grid-slideshow-display .grid-container {
width: 100%;
height: 100%;
border: none;
border-radius: 0;
}
.grid-slideshow-display .grid-cell {
border: 1px solid #333;
}
.grid-slideshow-info {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 10px 15px;
border-radius: 8px;
font-size: 0.9rem;
z-index: 10;
min-width: 200px;
opacity: 0.9;
transition: opacity 0.3s ease;
}
.grid-slideshow-info:hover {
opacity: 1;
}
.grid-slideshow-controls {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 15px;
background: rgba(0, 0, 0, 0.8);
padding: 15px 25px;
border-radius: 50px;
z-index: 10;
opacity: 0.9;
transition: opacity 0.3s ease;
}
.grid-slideshow-controls:hover {
opacity: 1;
}
.grid-slideshow-controls button {
background: var(--color-primary-hover);
color: white;
border: none;
border-radius: 25px;
padding: 10px 20px;
cursor: pointer;
font-size: 0.9rem;
transition: background 0.3s ease;
}
.grid-slideshow-controls button:hover {
background: var(--color-primary);
}
.grid-slideshow-controls button:disabled {
background: var(--color-primary-transparent);
cursor: not-allowed;
-webkit-appearance: none;
appearance: none;
}
.setting-control select {
background: rgba(0, 0, 0, 0.5);
color: #ffffff;
border: 1px solid var(--color-primary);
border-radius: 5px;
padding: 0.5rem;
width: 100%;
}
.setting-value {
color: var(--color-primary);
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: var(--color-primary);
}
/* 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 {
position: fixed;
bottom: 40px;
right: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
width: 80px;
height: 80px;
font-size: 0.75rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
box-shadow:
0 10px 30px rgba(102, 126, 234, 0.4),
0 0 0 0 rgba(102, 126, 234, 0.7),
inset 0 -3px 10px rgba(0, 0, 0, 0.2);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
line-height: 1.2;
padding: 0.5rem;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
box-shadow:
0 10px 30px rgba(102, 126, 234, 0.4),
0 0 0 0 rgba(102, 126, 234, 0.7),
inset 0 -3px 10px rgba(0, 0, 0, 0.2);
}
50% {
box-shadow:
0 10px 30px rgba(102, 126, 234, 0.6),
0 0 0 10px rgba(102, 126, 234, 0),
inset 0 -3px 10px rgba(0, 0, 0, 0.2);
}
}
.start-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #5a6fd8 0%, #6a42a0 100%);
box-shadow:
0 15px 40px rgba(102, 126, 234, 0.6),
0 0 20px rgba(102, 126, 234, 0.8),
inset 0 -3px 10px rgba(0, 0, 0, 0.3);
transform: scale(1.15) translateY(-3px);
border-color: rgba(255, 255, 255, 0.5);
}
.start-btn:active:not(:disabled) {
transform: scale(1.05) translateY(0px);
box-shadow:
0 5px 20px rgba(102, 126, 234, 0.4),
inset 0 -2px 8px rgba(0, 0, 0, 0.3);
}
.start-btn:disabled {
background: #6c757d;
cursor: not-allowed;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
animation: 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;
opacity: 0;
transition: opacity 0.3s ease-in-out;
pointer-events: none;
}
.slideshow-controls.visible {
opacity: 1;
pointer-events: auto;
}
.slideshow-container:hover .slideshow-controls {
opacity: 1;
pointer-events: auto;
}
.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;
opacity: 0;
transition: opacity 0.3s ease-in-out;
pointer-events: none;
}
.slideshow-info.visible {
opacity: 1;
pointer-events: auto;
}
.slideshow-container:hover .slideshow-info {
opacity: 1;
pointer-events: auto;
}
.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;
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
.progress-container.visible {
opacity: 1;
}
.slideshow-container:hover .progress-container {
opacity: 1;
}
.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;
}
/* Header Styles */
.hypno-page-header {
background: var(--header-bg);
border-bottom: 1px solid var(--header-border);
padding: 8px 0;
position: fixed;
top: 0;
left: 0;
right: 0;
width: 100%;
z-index: 1000;
box-sizing: border-box;
}
.hypno-nav {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0 15px;
}
.hypno-nav-left {
flex: 0 0 auto;
}
.hypno-nav-center {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.hypno-nav-right {
flex: 0 0 auto;
display: flex;
gap: 8px;
}
.hypno-nav h1 {
color: var(--header-title-color);
font-family: 'Audiowide', sans-serif;
font-size: 20px;
margin: 0;
text-shadow: var(--shadow-glow-primary);
}
.header-btn {
padding: 6px 12px;
font-size: 13px;
border-radius: 4px;
border: none;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 5px;
}
.header-btn-secondary {
background: var(--btn-secondary-bg);
color: var(--btn-secondary-text);
border: 1px solid var(--btn-secondary-border);
}
.header-btn-secondary:hover {
background: var(--btn-secondary-hover-bg);
border-color: var(--btn-secondary-hover-border);
}
/* Adjust main content for fixed header */
.main-content {
margin-top: 60px;
}
/* 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>
<!-- Header -->
<header class="hypno-page-header">
<div class="hypno-nav">
<div class="hypno-nav-left">
<h1>🔲 Grid Slideshow</h1>
</div>
<div class="hypno-nav-center">
</div>
<div class="hypno-nav-right">
<div id="theme-switcher-container"></div>
<a href="index.html" class="header-btn header-btn-secondary" title="Return to main menu">🏠 Home</a>
<a href="hypno-single.html" class="header-btn header-btn-secondary" title="Switch to single slideshow mode">🌀 Single</a>
</div>
</div>
</header>
<!-- Navigation -->
<div class="navigation" style="display: none;">
<a href="hypno-menu.html" class="back-btn">← Back to Gallery Menu</a>
</div>
<!-- Main Content -->
<div class="main-content">
<!-- Header -->
<div class="hypno-header">
<div id="theme-switcher-container" style="position: absolute; top: 20px; right: 20px;"></div>
<h1>🔲 Grid Hypno Slideshow</h1>
<div class="subtitle">Multiple simultaneous slideshows in synchronized grids</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: var(--color-primary); color: white; border: none; border-radius: 5px; cursor: pointer; display: none;">
🖼️ View Image Library
</button>
</div>
<!-- Grid Mode Selection -->
<div class="settings-panel">
<div class="settings-section">
<h3>🔲 Display Mode</h3>
<div class="setting-group">
<div class="setting-row">
<span class="setting-label">Layout Mode:</span>
<div class="setting-control">
<select id="displayMode" onchange="updateDisplayMode()">
<option value="grid-2x2">2x2 Grid</option>
<option value="grid-3x3">3x3 Grid</option>
</select>
</div>
</div>
</div>
</div>
</div>
<!-- Grid View Container (shown when layout selected) -->
<div id="gridView" style="display: none;">
<div class="grid-container single" id="gridContainer"></div>
</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>
<!-- 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>
</div>
<!-- Background Settings -->
<div class="settings-section">
<h3>🎨 Background Settings</h3>
<div class="setting-group">
<div class="setting-row">
<span class="setting-label">Background Type:</span>
<div class="setting-control">
<select id="backgroundType" style="width: 100%; padding: 0.5rem; background: rgba(0,0,0,0.3); border: 1px solid #667eea; border-radius: 5px; color: white;">
<option value="solid">Solid Color</option>
<option value="gradient">Gradient</option>
<option value="blurred">Blurred Image</option>
</select>
</div>
</div>
<!-- Solid Color Options -->
<div id="solidColorOptions" class="setting-row">
<span class="setting-label">Background Color:</span>
<div class="setting-control" style="display: flex; gap: 0.5rem; align-items: center;">
<input type="color" id="backgroundColor" value="#000000" style="width: 50px; height: 35px; border: 1px solid #667eea; border-radius: 5px; background: none;">
<input type="text" id="backgroundColorHex" value="#000000" placeholder="#000000" style="flex: 1; padding: 0.5rem; background: rgba(0,0,0,0.3); border: 1px solid #667eea; border-radius: 5px; color: white;">
</div>
</div>
<!-- Gradient Options -->
<div id="gradientOptions" class="setting-row" style="display: none;">
<span class="setting-label">Gradient Colors:</span>
<div class="setting-control">
<div style="display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem;">
<label style="color: #ccc; min-width: 60px;">From:</label>
<input type="color" id="gradientColor1" value="#000000" style="width: 50px; height: 35px; border: 1px solid #667eea; border-radius: 5px;">
<input type="text" id="gradientColor1Hex" value="#000000" style="flex: 1; padding: 0.5rem; background: rgba(0,0,0,0.3); border: 1px solid #667eea; border-radius: 5px; color: white;">
</div>
<div style="display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem;">
<label style="color: #ccc; min-width: 60px;">To:</label>
<input type="color" id="gradientColor2" value="#1a1a1a" style="width: 50px; height: 35px; border: 1px solid #667eea; border-radius: 5px;">
<input type="text" id="gradientColor2Hex" value="#1a1a1a" style="flex: 1; padding: 0.5rem; background: rgba(0,0,0,0.3); border: 1px solid #667eea; border-radius: 5px; color: white;">
</div>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<label style="color: #ccc; min-width: 60px;">Direction:</label>
<select id="gradientDirection" style="flex: 1; padding: 0.5rem; background: rgba(0,0,0,0.3); border: 1px solid #667eea; border-radius: 5px; color: white;">
<option value="to bottom">Top to Bottom</option>
<option value="to right">Left to Right</option>
<option value="to bottom right">Top-Left to Bottom-Right</option>
<option value="to bottom left">Top-Right to Bottom-Left</option>
<option value="45deg">Diagonal (45°)</option>
</select>
</div>
</div>
</div>
<!-- Blurred Image Options -->
<div id="blurredOptions" class="setting-row" style="display: none;">
<span class="setting-label">Blur Settings:</span>
<div class="setting-control">
<div style="display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem;">
<label style="color: #ccc; min-width: 80px;">Blur Amount:</label>
<input type="range" id="blurAmount" min="5" max="50" value="20" style="flex: 1;">
<span id="blurAmountValue" style="color: #667eea; min-width: 40px; text-align: right;">20px</span>
</div>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<label style="color: #ccc; min-width: 80px;">Opacity:</label>
<input type="range" id="blurOpacity" min="10" max="100" value="70" style="flex: 1;">
<span id="blurOpacityValue" style="color: #667eea; min-width: 40px; text-align: right;">70%</span>
</div>
</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>
<!-- Floating Start Button -->
<button class="start-btn" id="startSlideshowBtn" onclick="startGridSlideshow()" title="Start Grid Slideshow">
Start<br>Slideshow
</button>
</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>
<!-- Grid Slideshow Container (Fullscreen) -->
<div class="grid-slideshow-container" id="gridSlideshowContainer">
<div class="grid-slideshow-display" id="gridSlideshowDisplay"></div>
<div class="grid-slideshow-info" id="gridSlideshowInfo">
<div>Grid Mode: <span id="gridModeDisplay">2x2</span></div>
<div>Active Cells: <span id="activeCells">0</span></div>
<div>Status: <span id="gridSlideshowStatus">Ready</span></div>
</div>
<div class="grid-slideshow-controls">
<button onclick="pauseGridSlideshow()" id="gridPauseBtn">⏸️ Pause All</button>
<button onclick="nextGridAll()" id="gridNextBtn">⏭️ Next All</button>
<button onclick="stopGridSlideshow()" id="gridStopBtn">⏹️ Stop Grid</button>
</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,
backgroundType: 'solid',
backgroundColor: '#000000',
gradientColor1: '#000000',
gradientColor2: '#1a1a1a',
gradientDirection: 'to bottom',
blurAmount: 20,
blurOpacity: 70
};
// Slideshow Management Variables
let savedSlideshows = {};
let currentSlideshowName = null;
let slideshow = {
images: [],
currentIndex: 0,
isPlaying: false,
isPaused: false,
timer: null,
nextTimeout: null,
startTime: 0,
waveTime: 0
};
// Grid Slideshow Variables
let gridSlideshow = {
mode: 'single',
cells: [],
isPlaying: false,
isPaused: false,
configurations: {}
};
// GridCell Class
class GridCell {
constructor(index) {
this.index = index;
this.slideshow = null;
this.images = [];
this.currentIndex = 0;
this.timer = null;
this.element = null;
this.imageElement = null;
this.infoElement = null;
this.progressElement = null;
this.isPlaying = false;
// Copy current settings safely
this.settings = {
timingMode: currentSettings.timingMode,
duration: currentSettings.duration,
minDuration: currentSettings.minDuration,
maxDuration: currentSettings.maxDuration,
waveMin: currentSettings.waveMin,
waveMax: currentSettings.waveMax,
waveRate: currentSettings.waveRate,
transitionDuration: currentSettings.transitionDuration
};
console.log(`GridCell ${index} initialized with settings:`, this.settings);
}
initializeElement() {
this.element = document.createElement('div');
this.element.className = 'grid-cell';
const cellIndex = this.index;
this.element.innerHTML = `
<img class="grid-cell-image" alt="Grid Cell Image">
<div class="grid-cell-info">
<div>Cell ${cellIndex + 1}: <span class="cell-slideshow">Click to assign</span></div>
<div>Image: <span class="cell-current">0</span> / <span class="cell-total">0</span></div>
</div>
<div class="grid-cell-controls">
<button class="grid-cell-btn" onclick="pauseGridCell(${cellIndex})">⏸️</button>
<button class="grid-cell-btn" onclick="nextGridCell(${cellIndex})">⏭️</button>
</div>
<div class="grid-progress-container">
<div class="grid-progress-bar"></div>
</div>
`;
// Add click handler to assign slideshow
this.element.style.cursor = 'pointer';
this.element.addEventListener('click', (e) => {
// Don't trigger if clicking controls
if (!e.target.closest('.grid-cell-controls')) {
showGridCellSlideshowPicker(cellIndex);
}
});
this.imageElement = this.element.querySelector('.grid-cell-image');
this.infoElement = this.element.querySelector('.grid-cell-info');
return this.element;
}
async loadSlideshow(slideshow) {
console.log(`Cell ${this.index}: Loading slideshow:`, slideshow ? slideshow.name : 'null');
this.slideshow = slideshow;
if (slideshow) {
// Load images for this specific slideshow
this.images = await getGridCellImages(slideshow);
// Shuffle images for this cell to ensure randomness
this.images = this.shuffleArray([...this.images]);
// Start at a random position
this.currentIndex = this.images.length > 0 ? Math.floor(Math.random() * this.images.length) : 0;
} else {
this.images = [];
this.currentIndex = 0;
}
console.log(`Cell ${this.index}: Loaded ${this.images.length} images, starting at index ${this.currentIndex}`);
this.updateInfo();
this.displayCurrentImage();
}
shuffleArray(array) {
// Fisher-Yates shuffle for randomizing images
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
updateInfo() {
if (this.infoElement) {
const slideshowSpan = this.infoElement.querySelector('.cell-slideshow');
const currentSpan = this.infoElement.querySelector('.cell-current');
const totalSpan = this.infoElement.querySelector('.cell-total');
if (slideshowSpan) slideshowSpan.textContent = this.slideshow ? this.slideshow.name : 'Empty';
if (currentSpan) currentSpan.textContent = this.images.length > 0 ? this.currentIndex + 1 : 0;
if (totalSpan) totalSpan.textContent = this.images.length;
}
}
displayCurrentImage() {
if (this.images.length === 0 || !this.imageElement) return;
const imageData = this.images[this.currentIndex];
console.log(`Cell ${this.index}: Image data structure:`, imageData);
let imageSrc;
if (typeof imageData === 'string') {
imageSrc = imageData;
} else {
// Try different possible path properties in order of preference
imageSrc = imageData.cachedPath || imageData.fullPath || imageData.path || imageData.src || imageData.dataURL;
}
console.log(`Cell ${this.index}: Using image src: ${imageSrc}`);
if (!imageSrc) {
console.error(`Cell ${this.index}: No valid image source found in:`, imageData);
return;
}
this.imageElement.style.opacity = '0';
setTimeout(() => {
this.imageElement.src = imageSrc;
this.imageElement.style.opacity = '1';
this.imageElement.classList.add('visible');
}, this.settings.transitionDuration / 2);
}
nextImage() {
if (this.images.length === 0) return;
this.currentIndex = (this.currentIndex + 1) % this.images.length;
this.updateInfo();
this.displayCurrentImage();
}
startTimer() {
console.log(`Cell ${this.index}: Starting timer`);
if (this.timer) clearTimeout(this.timer);
const duration = this.calculateDuration();
console.log(`Cell ${this.index}: Duration = ${duration}ms`);
this.updateProgress(duration);
this.timer = setTimeout(() => {
console.log(`Cell ${this.index}: Timer fired, advancing image`);
this.nextImage();
if (this.isPlaying) {
this.startTimer();
}
}, duration);
}
calculateDuration() {
switch (this.settings.timingMode) {
case 'random':
return Math.random() * (this.settings.maxDuration - this.settings.minDuration) + this.settings.minDuration;
case 'wave':
const wavePosition = Date.now() / 1000 / this.settings.waveRate;
const waveValue = (Math.sin(wavePosition) + 1) / 2;
return this.settings.waveMin + (this.settings.waveMax - this.settings.waveMin) * waveValue;
default:
return this.settings.duration;
}
}
updateProgress(duration) {
if (!this.progressElement) return;
this.progressElement.style.transition = 'none';
this.progressElement.style.width = '0%';
setTimeout(() => {
this.progressElement.style.transition = `width ${duration}ms linear`;
this.progressElement.style.width = '100%';
}, 50);
}
pause() {
this.isPlaying = false;
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
resume() {
this.isPlaying = true;
this.startTimer();
}
stop() {
this.isPlaying = false;
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
this.currentIndex = 0;
this.updateInfo();
if (this.progressElement) {
this.progressElement.style.width = '0%';
}
}
}
// Grid Management Functions
function updateDisplayMode() {
const mode = document.getElementById('displayMode').value;
const gridView = document.getElementById('gridView');
if (mode === 'single') {
gridView.style.display = 'none';
stopGridSlideshow();
} else {
gridView.style.display = 'block';
setupGridLayout(mode);
}
gridSlideshow.mode = mode;
}
function setupGridLayout(mode) {
const gridContainer = document.getElementById('gridContainer');
// Clear existing grid
gridContainer.innerHTML = '';
gridSlideshow.cells = [];
// Set grid class
gridContainer.className = `grid-container ${mode}`;
// Create cells based on mode
let cellCount = 1;
switch (mode) {
case 'grid-2x2':
cellCount = 4;
break;
case 'grid-3x3':
cellCount = 9;
break;
default:
cellCount = 1;
}
// Create grid cells
for (let i = 0; i < cellCount; i++) {
const cell = new GridCell(i);
gridSlideshow.cells.push(cell);
const cellElement = cell.initializeElement();
gridContainer.appendChild(cellElement);
}
updateCellAssignments();
updateGridControls();
// Apply background settings to the grid container
setTimeout(() => applyBackground(), 50);
}
function showGridCellSlideshowPicker(cellIndex) {
const cell = gridSlideshow.cells[cellIndex];
if (!cell) return;
// Create modal overlay
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
`;
// Create modal content
const modal = document.createElement('div');
modal.style.cssText = `
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border: 2px solid #667eea;
border-radius: 15px;
padding: 2rem;
max-width: 500px;
width: 90%;
box-shadow: 0 10px 40px rgba(102, 126, 234, 0.3);
`;
modal.innerHTML = `
<h3 style="color: #667eea; margin: 0 0 1.5rem 0;">📋 Select Slideshow for Cell ${cellIndex + 1}</h3>
<div id="slideshowOptions" style="max-height: 400px; overflow-y: auto;">
${Object.keys(savedSlideshows).map(name => `
<div style="
background: rgba(102, 126, 234, 0.1);
border: 1px solid #667eea;
border-radius: 8px;
padding: 1rem;
margin-bottom: 0.5rem;
cursor: pointer;
transition: all 0.3s ease;
" onmouseover="this.style.background='rgba(102, 126, 234, 0.2)'"
onmouseout="this.style.background='rgba(102, 126, 234, 0.1)'"
onclick="assignSlideshowToCell(${cellIndex}, '${name}')">
<div style="color: white; font-weight: bold; margin-bottom: 0.25rem;">${name}</div>
<div style="color: #aaa; font-size: 0.9rem;">${savedSlideshows[name].images?.length || 0} images</div>
</div>
`).join('')}
<div style="
background: rgba(255, 100, 100, 0.1);
border: 1px solid #ff6464;
border-radius: 8px;
padding: 1rem;
margin-bottom: 0.5rem;
cursor: pointer;
transition: all 0.3s ease;
" onmouseover="this.style.background='rgba(255, 100, 100, 0.2)'"
onmouseout="this.style.background='rgba(255, 100, 100, 0.1)'"
onclick="assignSlideshowToCell(${cellIndex}, null)">
<div style="color: white; font-weight: bold;">Clear Assignment</div>
</div>
</div>
<button onclick="this.closest('.modal-overlay').remove()"
style="
margin-top: 1rem;
width: 100%;
padding: 0.75rem;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
transition: all 0.3s ease;
"
onmouseover="this.style.background='#5568d3'"
onmouseout="this.style.background='#667eea'">Close</button>
`;
overlay.className = 'modal-overlay';
overlay.appendChild(modal);
overlay.onclick = (e) => {
if (e.target === overlay) overlay.remove();
};
document.body.appendChild(overlay);
}
async function assignSlideshowToCell(cellIndex, slideshowName) {
const cell = gridSlideshow.cells[cellIndex];
if (!cell) return;
if (slideshowName) {
await cell.loadSlideshow(savedSlideshows[slideshowName]);
console.log(`✅ Assigned "${slideshowName}" to cell ${cellIndex}`);
} else {
await cell.loadSlideshow(null);
console.log(`🗑️ Cleared slideshow from cell ${cellIndex}`);
}
// Close modal
document.querySelector('.modal-overlay')?.remove();
}
function updateCellAssignments() {
// Legacy function - no longer needed but kept for compatibility
return;
}
async function startGridSlideshow() {
console.log('🟢 Starting grid slideshow...');
console.log('Grid cells:', gridSlideshow.cells.length);
if (gridSlideshow.cells.length === 0) {
alert('No grid cells configured');
return;
}
// Enter fullscreen grid mode
enterGridFullscreen();
gridSlideshow.isPlaying = true;
gridSlideshow.isPaused = false;
let activeCellCount = 0;
gridSlideshow.cells.forEach((cell, index) => {
console.log(`Cell ${index}: ${cell.images.length} images`);
if (cell.images.length > 0) {
console.log(`Starting cell ${index}`);
cell.isPlaying = true;
cell.startTimer();
activeCellCount++;
} else {
console.log(`Cell ${index} has no images, skipping`);
}
});
// Update fullscreen info
document.getElementById('gridModeDisplay').textContent = gridSlideshow.mode;
document.getElementById('activeCells').textContent = activeCellCount;
document.getElementById('gridSlideshowStatus').textContent = 'Playing';
updateGridControls();
console.log('✅ Grid slideshow start complete');
}
function pauseGridSlideshow() {
if (gridSlideshow.isPaused) {
resumeGridSlideshow();
return;
}
gridSlideshow.isPaused = true;
gridSlideshow.cells.forEach(cell => {
cell.pause();
});
updateGridControls();
}
function resumeGridSlideshow() {
gridSlideshow.isPaused = false;
gridSlideshow.cells.forEach(cell => {
if (cell.images.length > 0) {
cell.resume();
}
});
updateGridControls();
}
function stopGridSlideshow() {
gridSlideshow.isPlaying = false;
gridSlideshow.isPaused = false;
gridSlideshow.cells.forEach(cell => {
cell.stop();
});
// Exit fullscreen mode
exitGridFullscreen();
// Update status
const gridStatus = document.getElementById('gridSlideshowStatus');
if (gridStatus) {
gridStatus.textContent = 'Stopped';
}
updateGridControls();
console.log('🔲 Grid slideshow stopped');
}
// Fullscreen grid control functions
function pauseGridSlideshow() {
if (!gridSlideshow.isPlaying) return;
gridSlideshow.isPaused = !gridSlideshow.isPaused;
gridSlideshow.cells.forEach(cell => {
if (gridSlideshow.isPaused) {
cell.pause();
} else {
cell.resume();
}
});
// Update status
const gridStatus = document.getElementById('gridSlideshowStatus');
if (gridStatus) {
gridStatus.textContent = gridSlideshow.isPaused ? 'Paused' : 'Playing';
}
updateGridControls();
console.log('⏸️ Grid slideshow', gridSlideshow.isPaused ? 'paused' : 'resumed');
}
function nextGridAll() {
if (!gridSlideshow.isPlaying) return;
gridSlideshow.cells.forEach(cell => {
if (cell.images.length > 0) {
cell.next();
}
});
console.log('⏭️ All grid cells advanced to next image');
}
function nextGridAll() {
gridSlideshow.cells.forEach(cell => {
if (cell.images.length > 0) {
cell.nextImage();
}
});
}
function pauseGridCell(cellIndex) {
const cell = gridSlideshow.cells[cellIndex];
if (cell) {
if (cell.isPlaying) {
cell.pause();
} else {
cell.resume();
}
}
}
function nextGridCell(cellIndex) {
const cell = gridSlideshow.cells[cellIndex];
if (cell && cell.images.length > 0) {
cell.nextImage();
}
}
function updateGridControls() {
const startBtn = document.getElementById('startGridBtn');
const pauseBtn = document.getElementById('pauseGridBtn');
const stopBtn = document.getElementById('stopGridBtn');
if (startBtn && pauseBtn && stopBtn) {
if (gridSlideshow.isPlaying) {
startBtn.disabled = true;
pauseBtn.disabled = false;
pauseBtn.textContent = gridSlideshow.isPaused ? '▶️ Resume Grid' : '⏸️ Pause Grid';
stopBtn.disabled = false;
} else {
startBtn.disabled = false;
pauseBtn.disabled = true;
pauseBtn.textContent = '⏸️ Pause Grid';
stopBtn.disabled = true;
}
}
// Update fullscreen controls if visible
const gridPauseBtn = document.getElementById('gridPauseBtn');
const gridStopBtn = document.getElementById('gridStopBtn');
if (gridPauseBtn && gridStopBtn) {
if (gridSlideshow.isPlaying) {
gridPauseBtn.disabled = false;
gridPauseBtn.textContent = gridSlideshow.isPaused ? '▶️ Resume All' : '⏸️ Pause All';
gridStopBtn.disabled = false;
} else {
gridPauseBtn.disabled = true;
gridPauseBtn.textContent = '⏸️ Pause All';
gridStopBtn.disabled = true;
}
}
}
function enterGridFullscreen() {
console.log('🔲 Entering grid fullscreen mode');
// Hide main content
document.querySelector('.main-content').style.display = 'none';
// Show grid fullscreen container
document.getElementById('gridSlideshowContainer').style.display = 'flex';
// Move grid container to fullscreen display
const gridContainer = document.getElementById('gridContainer');
const fullscreenDisplay = document.getElementById('gridSlideshowDisplay');
if (gridContainer && fullscreenDisplay) {
fullscreenDisplay.appendChild(gridContainer);
}
}
function exitGridFullscreen() {
console.log('🔲 Exiting grid fullscreen mode');
// Hide grid fullscreen container
document.getElementById('gridSlideshowContainer').style.display = 'none';
// Show main content
document.querySelector('.main-content').style.display = 'block';
// Move grid container back to settings
const gridContainer = document.getElementById('gridContainer');
const gridView = document.getElementById('gridView');
if (gridContainer && gridView) {
// Find the original position (after the controls)
const gridControls = gridView.querySelector('.grid-controls');
if (gridControls && gridControls.nextElementSibling) {
gridView.insertBefore(gridContainer, gridControls.nextElementSibling);
} else {
gridView.appendChild(gridContainer);
}
}
}
// 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;
}
console.log('💾 Loaded directory preferences:', currentSettings.selectedDirectories);
// Populate directory list
// populateDirectoryList(linkedDirectories); // UI element removed
} catch (error) {
console.error('❌ Error initializing directory selection:', error);
}
}
// Populate Directory List
function populateDirectoryList(directories) {
const directoriesList = document.getElementById('linkedDirectoriesList');
if (!directoriesList) {
console.log('📁 Directory list UI element not found (removed), skipping population');
return;
}
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`);
} else {
// Check for popup image library data from localStorage
const popupImageLibrary = localStorage.getItem('popupImageLibrary');
if (popupImageLibrary) {
try {
let unifiedLibrary = JSON.parse(popupImageLibrary);
// Filter by selected directories if any are selected
if (currentSettings.selectedDirectories.length > 0) {
unifiedLibrary = unifiedLibrary.filter(image => {
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 popup library to ${unifiedLibrary.length} images from selected directories`);
}
imageLibrary = [...unifiedLibrary];
console.log(`🖼️ Using popup image library: ${imageLibrary.length} images`);
} catch (error) {
console.error('Failed to parse popup image library:', error);
}
}
}
if (imageLibrary.length === 0) {
const message = currentSettings.selectedDirectories.length > 0
? '⚠️ No images found in selected directories'
: '⚠️ Image library unavailable - Open from Quick Play to load library';
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('transitionType').addEventListener('change', (e) => {
currentSettings.transitionType = e.target.value;
updateTransitionSettings(e.target.value);
});
// Include captured photos checkbox (removed from UI)
const capturedPhotosCheckbox = document.getElementById('includeCapturedPhotos');
if (capturedPhotosCheckbox) {
capturedPhotosCheckbox.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 (removed from UI)
const refreshButton = document.getElementById('refreshDirectories');
if (refreshButton) {
refreshButton.addEventListener('click', async () => {
console.log('🔄 Refreshing directories...');
await initializeDirectorySelection();
await initializeImageLibrary();
});
}
// Background settings
document.getElementById('backgroundType').addEventListener('change', (e) => {
currentSettings.backgroundType = e.target.value;
updateBackgroundOptions(e.target.value);
applyBackground();
saveHypnoGallerySettings();
});
// Color pickers and hex inputs
setupColorControl('backgroundColor');
setupColorControl('gradientColor1');
setupColorControl('gradientColor2');
document.getElementById('gradientDirection').addEventListener('change', (e) => {
currentSettings.gradientDirection = e.target.value;
applyBackground();
saveHypnoGallerySettings();
});
// Blur controls
document.getElementById('blurAmount').addEventListener('input', (e) => {
const value = parseInt(e.target.value);
currentSettings.blurAmount = value;
document.getElementById('blurAmountValue').textContent = value + 'px';
applyBackground();
saveHypnoGallerySettings();
});
document.getElementById('blurOpacity').addEventListener('input', (e) => {
const value = parseInt(e.target.value);
currentSettings.blurOpacity = value;
document.getElementById('blurOpacityValue').textContent = value + '%';
applyBackground();
saveHypnoGallerySettings();
});
// Keyboard controls
document.addEventListener('keydown', handleKeyPress);
// Mouse movement controls for auto-hiding UI
setupMouseControls();
}
// 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
async function startSlideshow() {
console.log('🌀 Starting Hypno Gallery slideshow...');
// Get slideshow-specific images
const slideshowImages = await getSlideshowImages();
if (slideshowImages.length === 0) {
alert('No images available for slideshow. Please configure slideshow directories or add images to your library first.');
return;
}
// Prepare image array
slideshow.images = [...slideshowImages];
// 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}`;
}
// Update background for blurred mode
if (currentSettings.backgroundType === 'blurred') {
// Wait for image to load before updating background
imgElement.onload = () => {
setTimeout(() => applyBackground(), 100); // Small delay to ensure image is rendered
};
}
// 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;
// Cancel current timing to restart timer
if (slideshow.nextTimeout) {
clearTimeout(slideshow.nextTimeout);
}
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) {
// Allow keyboard controls even when slideshow is paused
if (!slideshow.isPlaying && !slideshow.isPaused) return;
switch (event.key) {
case ' ':
case 'p':
case 'k': // Video player convention
event.preventDefault();
pauseSlideshow();
break;
case 'ArrowRight':
case 'n':
case 'l': // Right arrow alternative
case 'j': // Vim-like forward
event.preventDefault();
nextImage();
break;
case 'ArrowLeft':
case 'b':
case 'h': // Left arrow alternative
case 'k': // Vim-like backward (if not pausing)
event.preventDefault();
if (event.key === 'k' && slideshow.isPlaying) {
pauseSlideshow(); // k pauses when playing
} else {
previousImage();
}
break;
case 'Escape':
case 'q':
case 'x':
event.preventDefault();
stopSlideshow();
break;
case 'f':
case 'F11':
event.preventDefault();
toggleFullscreen();
break;
case 'r':
event.preventDefault();
// Restart slideshow from beginning
slideshow.currentIndex = 0;
displayCurrentImage();
break;
case '?':
case 'h':
event.preventDefault();
showKeyboardHelp();
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]];
}
}
// Mouse control setup for auto-hiding UI
let mouseTimeout;
let controlsVisible = false;
function setupMouseControls() {
const slideshowContainer = document.getElementById('slideshowContainer');
if (!slideshowContainer) return;
// Show controls on mouse movement
slideshowContainer.addEventListener('mousemove', () => {
if (!slideshow.isPlaying && !slideshow.isPaused) return;
showControls();
// Clear existing timeout
if (mouseTimeout) {
clearTimeout(mouseTimeout);
}
// Hide controls after 3 seconds of no movement
mouseTimeout = setTimeout(() => {
hideControls();
}, 3000);
});
// Keep controls visible when hovering over them
const controls = slideshowContainer.querySelector('.slideshow-controls');
const info = slideshowContainer.querySelector('.slideshow-info');
[controls, info].forEach(element => {
if (element) {
element.addEventListener('mouseenter', () => {
if (mouseTimeout) {
clearTimeout(mouseTimeout);
}
showControls();
});
element.addEventListener('mouseleave', () => {
mouseTimeout = setTimeout(() => {
hideControls();
}, 1000);
});
}
});
// Controls start hidden and only show on mouse hover
// No initial hideControls() call needed as CSS handles this
}
function showControls() {
controlsVisible = true;
const slideshowContainer = document.getElementById('slideshowContainer');
if (!slideshowContainer) return;
const controls = slideshowContainer.querySelector('.slideshow-controls');
const info = slideshowContainer.querySelector('.slideshow-info');
const progress = slideshowContainer.querySelector('.progress-container');
if (controls) controls.classList.add('visible');
if (info) info.classList.add('visible');
if (progress && progress.style.display !== 'none') progress.classList.add('visible');
}
function hideControls() {
controlsVisible = false;
const slideshowContainer = document.getElementById('slideshowContainer');
if (!slideshowContainer) return;
const controls = slideshowContainer.querySelector('.slideshow-controls');
const info = slideshowContainer.querySelector('.slideshow-info');
const progress = slideshowContainer.querySelector('.progress-container');
if (controls) controls.classList.remove('visible');
if (info) info.classList.remove('visible');
if (progress) progress.classList.remove('visible');
}
function showControlsTemporarily() {
// Keyboard controls no longer show UI - only mouse hover does
// This function kept for potential future use
return;
}
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => {
console.log('Error attempting to enable fullscreen:', err);
});
} else {
document.exitFullscreen().catch(err => {
console.log('Error attempting to exit fullscreen:', err);
});
}
}
function showKeyboardHelp() {
const helpText = `
🎮 KEYBOARD CONTROLS:
▶️ PLAYBACK:
Space, P, K - Pause/Resume
→, N, L, J - Next image
←, B, H - Previous image
R - Restart from beginning
🎯 NAVIGATION:
Esc, Q, X - Stop slideshow
F, F11 - Toggle fullscreen
❓ HELP:
?, H - Show this help
Controls auto-hide after 3 seconds of no mouse movement.
Move mouse or use keyboard to show them again.
`;
alert(helpText);
}
// 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);
}
// Slideshow Management Functions
function loadSavedSlideshows() {
try {
const saved = localStorage.getItem('hypnoGallerySlideshows');
if (saved) {
savedSlideshows = JSON.parse(saved);
console.log('📋 Loaded slideshows:', Object.keys(savedSlideshows).length);
} else {
savedSlideshows = {};
console.log('📋 No saved slideshows found, initialized empty object');
}
// Refresh grid cell assignments if in grid mode
if (gridSlideshow.mode !== 'single' && gridSlideshow.cells.length > 0) {
updateCellAssignments();
}
} catch (error) {
console.error('❌ Error loading slideshows:', error);
savedSlideshows = {};
}
}
function saveSlideshowsToStorage() {
try {
localStorage.setItem('hypnoGallerySlideshows', JSON.stringify(savedSlideshows));
console.log('💾 Slideshows saved to storage');
// Refresh grid cell assignments if in grid mode
if (gridSlideshow.mode !== 'single' && gridSlideshow.cells.length > 0) {
updateCellAssignments();
}
} catch (error) {
console.error('❌ Error saving slideshows:', error);
}
}
function updateSlideshowsList() {
// This function is kept for compatibility but no longer manages dropdown
console.log('📋 Slideshows available:', Object.keys(savedSlideshows || {}).length);
// Update slideshow details if current slideshow is loaded
updateSlideshowDetails();
}
function updateSlideshowDetails() {
const details = document.getElementById('slideshowDetails');
const directoryList = document.getElementById('slideshowDirectoryList');
const addBtn = document.getElementById('addDirectoryBtn');
const removeBtn = document.getElementById('removeDirectoryBtn');
const testBtn = document.getElementById('testDirectoriesBtn');
if (currentSlideshowName && savedSlideshows[currentSlideshowName]) {
const slideshow = savedSlideshows[currentSlideshowName];
const directories = slideshow.directories || [];
const sourcesCount = directories.length + (slideshow.includeCapturedPhotos ? 1 : 0);
details.innerHTML = `
<div style="font-size: 1.1em; font-weight: bold; color: #667eea; margin-bottom: 0.5rem; border-bottom: 1px solid rgba(102, 126, 234, 0.3); padding-bottom: 0.3rem;">
🎬 ${currentSlideshowName}
</div>
<div>📁 Directories: ${directories.length} folders</div>
<div>📷 Captured Photos: ${slideshow.includeCapturedPhotos ? 'Included' : 'Excluded'}</div>
<div>⏱️ Timing: ${slideshow.settings.timingMode} (${slideshow.settings.duration}ms)</div>
<div>✨ Transition: ${slideshow.settings.transitionType} (${slideshow.settings.transitionDuration}ms)</div>
<div>📅 Created: ${slideshow.created ? new Date(slideshow.created).toLocaleDateString() : 'Unknown'}</div>
`;
// Update directory list
if (directories.length > 0) {
directoryList.innerHTML = directories.map((dir, index) => `
<div onclick="selectSlideshowDirectory(${index})" class="slideshow-directory-item" data-index="${index}"
style="padding: 0.25rem 0.5rem; margin: 0.2rem 0; background: rgba(102, 126, 234, 0.1); border-radius: 3px; cursor: pointer; border: 1px solid transparent; color: #ccc; font-size: 0.9rem;">
📁 ${dir.name || dir}
</div>
`).join('');
} else {
directoryList.innerHTML = '<div style="color: #999; font-style: italic;">No directories added</div>';
}
// Enable buttons
addBtn.disabled = false;
document.getElementById('addFromLibraryBtn').disabled = false;
removeBtn.disabled = true; // Enable when directory is selected
testBtn.disabled = directories.length === 0;
} else {
details.innerHTML = '<div>Select a slideshow to view details</div>';
directoryList.innerHTML = '<div style="color: #999; font-style: italic;">Select a slideshow to manage directories</div>';
// Disable buttons
addBtn.disabled = true;
document.getElementById('addFromLibraryBtn').disabled = true;
removeBtn.disabled = true;
testBtn.disabled = true;
}
}
function showCreateSlideshowDialog() {
const dialog = document.createElement('div');
dialog.id = 'create-slideshow-dialog';
dialog.innerHTML = `
<div style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 10000;">
<div style="background: linear-gradient(145deg, #1a0d2e, #2d1b3d); border: 2px solid #667eea; border-radius: 15px; padding: 2rem; max-width: 400px; width: 90%; box-shadow: 0 20px 40px rgba(102, 126, 234, 0.3);">
<h3 style="color: #667eea; margin: 0 0 1.5rem 0; text-align: center;">🎬 Create New Slideshow</h3>
<div style="margin-bottom: 1.5rem;">
<label style="color: #ccc; display: block; margin-bottom: 0.5rem;">Slideshow Name:</label>
<input type="text" id="dialogSlideshowName" placeholder="Enter slideshow name..."
style="width: 100%; box-sizing: border-box; padding: 0.75rem; background: rgba(0,0,0,0.3); border: 1px solid #667eea; border-radius: 8px; color: white; font-size: 1rem; outline: none; position: relative; z-index: 10001;"
autocomplete="off" spellcheck="false">
</div>
<div style="display: flex; gap: 1rem; justify-content: center;">
<button onclick="createSlideshowFromDialog()"
style="background: #667eea; color: white; border: none; border-radius: 8px; padding: 0.75rem 1.5rem; cursor: pointer; font-size: 1rem;">
✅ Create
</button>
<button onclick="closeSlideshowDialog()"
style="background: #666; color: white; border: none; border-radius: 8px; padding: 0.75rem 1.5rem; cursor: pointer; font-size: 1rem;">
❌ Cancel
</button>
</div>
</div>
</div>
`;
document.body.appendChild(dialog);
// Focus the input and select all text with multiple attempts
const focusInput = () => {
const input = document.getElementById('dialogSlideshowName');
if (input) {
input.focus();
input.select();
// Add click handler to ensure input can be clicked
input.addEventListener('click', () => {
input.focus();
});
// Add a safety focus on any dialog click
dialog.addEventListener('click', (e) => {
if (e.target === input) {
input.focus();
}
});
}
};
// Try multiple times to ensure focus works
setTimeout(focusInput, 50);
setTimeout(focusInput, 150);
setTimeout(focusInput, 300);
// Handle Enter key
document.getElementById('dialogSlideshowName').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
createSlideshowFromDialog();
} else if (e.key === 'Escape') {
closeSlideshowDialog();
}
});
}
function showLoadSlideshowDialog() {
console.log('🔍 Debug - savedSlideshows:', savedSlideshows);
console.log('🔍 Debug - slideshow count:', savedSlideshows ? Object.keys(savedSlideshows).length : 'savedSlideshows is null/undefined');
console.log('🔍 Debug - localStorage check:', localStorage.getItem('hypnoGallerySlideshows'));
// Reload slideshows if empty but localStorage has data
if ((!savedSlideshows || Object.keys(savedSlideshows).length === 0)) {
const saved = localStorage.getItem('hypnoGallerySlideshows');
if (saved) {
try {
savedSlideshows = JSON.parse(saved);
console.log('🔄 Reloaded slideshows from localStorage:', Object.keys(savedSlideshows).length);
} catch (e) {
console.error('❌ Error parsing saved slideshows:', e);
}
}
}
if (!savedSlideshows || Object.keys(savedSlideshows).length === 0) {
alert('No saved slideshows available. Create one first!');
return;
}
const dialog = document.createElement('div');
dialog.id = 'load-slideshow-dialog';
dialog.innerHTML = `
<div style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 10000;">
<div style="background: linear-gradient(145deg, #1a0d2e, #2d1b3d); border: 2px solid #4CAF50; border-radius: 15px; padding: 2rem; max-width: 400px; width: 90%; box-shadow: 0 20px 40px rgba(76, 175, 80, 0.3);">
<h3 style="color: #4CAF50; margin: 0 0 1.5rem 0; text-align: center;">📂 Load Slideshow</h3>
<div style="margin-bottom: 1.5rem;">
<label style="color: #ccc; display: block; margin-bottom: 0.5rem;">Select Slideshow:</label>
<select id="dialogSlideshowSelect"
style="width: 100%; padding: 0.75rem; background: rgba(0,0,0,0.3); border: 1px solid #4CAF50; border-radius: 8px; color: white; font-size: 1rem;">
<option value="">Choose a slideshow...</option>
${Object.keys(savedSlideshows).map(name =>
`<option value="${name}">${name}</option>`
).join('')}
</select>
</div>
<div id="dialogSlideshowInfo" style="margin-bottom: 1.5rem; color: #ccc; font-size: 0.9rem; min-height: 60px;">
Select a slideshow to view details
</div>
<div style="display: flex; gap: 1rem; justify-content: center;">
<button onclick="loadSlideshowFromDialog()" id="dialogLoadBtn" disabled
style="background: #4CAF50; color: white; border: none; border-radius: 8px; padding: 0.75rem 1.5rem; cursor: pointer; font-size: 1rem;">
📂 Load
</button>
<button onclick="closeSlideshowDialog()"
style="background: #666; color: white; border: none; border-radius: 8px; padding: 0.75rem 1.5rem; cursor: pointer; font-size: 1rem;">
❌ Cancel
</button>
</div>
</div>
</div>
`;
document.body.appendChild(dialog);
// Add change listener for slideshow selection
document.getElementById('dialogSlideshowSelect').addEventListener('change', updateDialogSlideshowInfo);
}
function updateDialogSlideshowInfo() {
const select = document.getElementById('dialogSlideshowSelect');
const info = document.getElementById('dialogSlideshowInfo');
const loadBtn = document.getElementById('dialogLoadBtn');
if (select.value && savedSlideshows[select.value]) {
const slideshow = savedSlideshows[select.value];
const directories = slideshow.directories || [];
info.innerHTML = `
<div>📁 Directories: ${directories.length} folders</div>
<div>📷 Captured Photos: ${slideshow.includeCapturedPhotos ? 'Included' : 'Excluded'}</div>
<div>⏱️ Timing: ${slideshow.settings.timingMode}</div>
<div>📅 Created: ${slideshow.created ? new Date(slideshow.created).toLocaleDateString() : 'Unknown'}</div>
`;
loadBtn.disabled = false;
} else {
info.textContent = 'Select a slideshow to view details';
loadBtn.disabled = true;
}
}
function createSlideshowFromDialog() {
const nameInput = document.getElementById('dialogSlideshowName');
const name = nameInput.value.trim();
if (!name) {
alert('Please enter a slideshow name');
nameInput.focus();
return;
}
if (savedSlideshows[name]) {
if (!confirm(`Slideshow "${name}" already exists. Replace it?`)) {
nameInput.focus();
return;
}
}
// Create slideshow with current settings and empty directories
savedSlideshows[name] = {
name: name,
settings: { ...currentSettings },
directories: [], // Slideshow-specific directories
includeCapturedPhotos: true, // Default to including captured photos
created: Date.now(),
modified: Date.now()
};
saveSlideshowsToStorage();
updateSlideshowsList();
// Load the new slideshow
loadSlideshow(name);
closeSlideshowDialog();
console.log('✅ Created slideshow:', name);
alert(`Slideshow "${name}" created successfully!`);
}
function loadSlideshowFromDialog() {
const select = document.getElementById('dialogSlideshowSelect');
const name = select.value;
if (!name || !savedSlideshows[name]) {
alert('Please select a slideshow to load');
return;
}
// Load the slideshow directly
loadSlideshow(name);
closeSlideshowDialog();
console.log('📂 Loaded slideshow from dialog:', name);
}
function closeSlideshowDialog() {
const createDialog = document.getElementById('create-slideshow-dialog');
const loadDialog = document.getElementById('load-slideshow-dialog');
if (createDialog) createDialog.remove();
if (loadDialog) loadDialog.remove();
}
function loadSlideshow(slideshowName = null) {
// If no name provided and no current slideshow, return
if (!slideshowName && !currentSlideshowName) {
currentSlideshowName = null;
updateCurrentSessionDisplay();
return;
}
const name = slideshowName || currentSlideshowName;
if (!name || !savedSlideshows[name]) {
// Clear settings if no slideshow selected
currentSlideshowName = null;
updateCurrentSessionDisplay();
return;
}
const slideshow = savedSlideshows[name];
// Load settings
Object.assign(currentSettings, slideshow.settings);
currentSlideshowName = name;
// Update UI controls
updateUIFromSettings();
updateCurrentSessionDisplay();
updateSlideshowDetails();
// Refresh grid cell assignments if in grid mode
if (gridSlideshow.mode !== 'single' && gridSlideshow.cells.length > 0) {
updateCellAssignments();
}
console.log('📂 Auto-loaded slideshow:', name);
}
function saveCurrentAsSlideshow() {
if (!currentSlideshowName || !savedSlideshows[currentSlideshowName]) {
alert('Please load a slideshow first, or create a new one');
return;
}
if (!confirm(`Update slideshow "${currentSlideshowName}" with current settings?`)) {
return;
}
savedSlideshows[currentSlideshowName].settings = { ...currentSettings };
savedSlideshows[currentSlideshowName].modified = Date.now();
saveSlideshowsToStorage();
updateSlideshowDetails();
console.log('💾 Updated slideshow:', currentSlideshowName);
alert(`Slideshow "${currentSlideshowName}" updated successfully!`);
}
function duplicateSlideshow() {
if (!currentSlideshowName || !savedSlideshows[currentSlideshowName]) {
alert('Please load a slideshow first');
return;
}
showDuplicateSlideshowDialog(currentSlideshowName);
}
function showDuplicateSlideshowDialog(originalName) {
const dialog = document.createElement('div');
dialog.id = 'duplicate-slideshow-dialog';
dialog.innerHTML = `
<div style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 10000;">
<div style="background: linear-gradient(145deg, #1a0d2e, #2d1b3d); border: 2px solid #2196F3; border-radius: 15px; padding: 2rem; max-width: 400px; width: 90%; box-shadow: 0 20px 40px rgba(33, 150, 243, 0.3);">
<h3 style="color: #2196F3; margin: 0 0 1.5rem 0; text-align: center;">📋 Duplicate Slideshow</h3>
<div style="margin-bottom: 1rem; color: #ccc; text-align: center;">
Duplicating: <span style="color: #2196F3; font-weight: bold;">${originalName}</span>
</div>
<div style="margin-bottom: 1.5rem;">
<label style="color: #ccc; display: block; margin-bottom: 0.5rem;">New Slideshow Name:</label>
<input type="text" id="dialogDuplicateName" placeholder="Enter new name..." value="${originalName} Copy"
style="width: 100%; box-sizing: border-box; padding: 0.75rem; background: rgba(0,0,0,0.3); border: 1px solid #2196F3; border-radius: 8px; color: white; font-size: 1rem; outline: none; position: relative; z-index: 10001;"
autocomplete="off" spellcheck="false">
</div>
<div style="display: flex; gap: 1rem; justify-content: center;">
<button onclick="executeDuplicate()"
style="background: #2196F3; color: white; border: none; border-radius: 8px; padding: 0.75rem 1.5rem; cursor: pointer; font-size: 1rem;">
✅ Duplicate
</button>
<button onclick="closeDuplicateDialog()"
style="background: #666; color: white; border: none; border-radius: 8px; padding: 0.75rem 1.5rem; cursor: pointer; font-size: 1rem;">
❌ Cancel
</button>
</div>
</div>
</div>
`;
document.body.appendChild(dialog);
// Focus the input and select all text
const focusInput = () => {
const input = document.getElementById('dialogDuplicateName');
if (input) {
input.focus();
input.select();
}
};
setTimeout(focusInput, 50);
setTimeout(focusInput, 150);
// Handle Enter key
document.getElementById('dialogDuplicateName').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
executeDuplicate();
} else if (e.key === 'Escape') {
closeDuplicateDialog();
}
});
}
function executeDuplicate() {
const input = document.getElementById('dialogDuplicateName');
const newName = input.value.trim();
const originalName = currentSlideshowName;
if (!newName) {
alert('Please enter a name for the duplicate slideshow');
input.focus();
return;
}
if (savedSlideshows[newName]) {
if (!confirm(`Slideshow "${newName}" already exists. Replace it?`)) {
input.focus();
return;
}
}
// Perform the duplication
savedSlideshows[newName] = {
...savedSlideshows[originalName],
name: newName,
created: Date.now(),
modified: Date.now()
};
saveSlideshowsToStorage();
updateSlideshowsList();
// Load the new slideshow
loadSlideshow(newName);
closeDuplicateDialog();
console.log('📋 Duplicated slideshow:', originalName, '→', newName);
alert(`Slideshow duplicated as "${newName}"!`);
}
function closeDuplicateDialog() {
const dialog = document.getElementById('duplicate-slideshow-dialog');
if (dialog) dialog.remove();
}
function showLibrarySelectionDialog() {
if (!currentSlideshowName || !savedSlideshows[currentSlideshowName]) {
alert('Please load a slideshow first');
return;
}
const dialog = document.createElement('div');
dialog.id = 'library-selection-dialog';
dialog.innerHTML = `
<div style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 10000;">
<div style="background: linear-gradient(145deg, #1a0d2e, #2d1b3d); border: 2px solid #FF9800; border-radius: 15px; padding: 2rem; max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 20px 40px rgba(255, 152, 0, 0.3);">
<h3 style="color: #FF9800; margin: 0 0 1.5rem 0; text-align: center;">📚 Add from Library</h3>
<div style="margin-bottom: 1rem;">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem;">
<input type="checkbox" id="libraryCapturedPhotos" style="margin: 0;">
<label for="libraryCapturedPhotos" style="color: #ccc; margin: 0;">📷 Include Captured Photos</label>
</div>
<div style="margin-bottom: 1rem;">
<label style="color: #ccc; display: block; margin-bottom: 0.5rem;">📁 Available Directories:</label>
<div id="libraryDirectoriesList" style="max-height: 300px; overflow-y: auto; background: rgba(0,0,0,0.3); border: 1px solid #FF9800; border-radius: 8px; padding: 0.5rem;">
Loading directories...
</div>
</div>
</div>
<div style="display: flex; gap: 1rem; justify-content: center;">
<button onclick="addSelectedFromLibrary()"
style="background: #FF9800; color: white; border: none; border-radius: 8px; padding: 0.75rem 1.5rem; cursor: pointer; font-size: 1rem;">
✅ Add Selected
</button>
<button onclick="closeLibraryDialog()"
style="background: #666; color: white; border: none; border-radius: 8px; padding: 0.75rem 1.5rem; cursor: pointer; font-size: 1rem;">
❌ Cancel
</button>
</div>
</div>
</div>
`;
document.body.appendChild(dialog);
// Load available directories
loadLibraryDirectories();
}
async function loadLibraryDirectories() {
const container = document.getElementById('libraryDirectoriesList');
try {
// Get linked directories from localStorage
const linkedDirectories = JSON.parse(localStorage.getItem('linkedImageDirectories') || '[]');
console.log('📚 Loading library directories:', linkedDirectories);
if (linkedDirectories.length === 0) {
container.innerHTML = '<div style="color: #999; font-style: italic; text-align: center; padding: 1rem;">No directories in library. Add some through the main game interface first.</div>';
return;
}
// Create checkboxes for each directory
container.innerHTML = linkedDirectories.map((dir, index) => {
const dirPath = dir.path || dir;
const dirName = typeof dir === 'string' ? dir.split(/[\\\\/]/).pop() : (dir.name || dirPath.split(/[\\\\/]/).pop() || 'Unknown Directory');
const imageCount = dir.imageCount || dir.images?.length || 0;
return `
<div style="display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; margin: 0.2rem 0; background: rgba(255, 152, 0, 0.1); border-radius: 5px; border: 1px solid transparent; cursor: pointer;"
onclick="toggleLibraryDirectory(${index})">
<input type="checkbox" id="libDir_${index}" style="margin: 0;">
<label for="libDir_${index}" style="color: #ccc; flex: 1; cursor: pointer; margin: 0;">
📁 ${dirName} <span style="color: #FF9800;">(${imageCount} images)</span>
</label>
</div>
`;
}).join('');
} catch (error) {
console.error('❌ Error loading library directories:', error);
container.innerHTML = '<div style="color: #f44336; text-align: center; padding: 1rem;">Error loading directories</div>';
}
}
function toggleLibraryDirectory(index) {
const checkbox = document.getElementById(`libDir_${index}`);
if (checkbox) {
checkbox.checked = !checkbox.checked;
}
}
function addSelectedFromLibrary() {
const includeCapturedPhotos = document.getElementById('libraryCapturedPhotos').checked;
const selectedDirectories = [];
// Get selected directories from localStorage
const linkedDirectories = JSON.parse(localStorage.getItem('linkedImageDirectories') || '[]');
linkedDirectories.forEach((dir, index) => {
const checkbox = document.getElementById(`libDir_${index}`);
if (checkbox && checkbox.checked) {
const dirPath = dir.path || dir;
selectedDirectories.push({
path: dirPath,
name: typeof dir === 'string' ? dir.split(/[\\\\/]/).pop() : (dir.name || dirPath.split(/[\\\\/]/).pop() || 'Unknown Directory')
});
}
});
// Allow adding nothing (user might want slideshow-specific directories only)
if (selectedDirectories.length === 0 && !includeCapturedPhotos) {
if (!confirm('No library directories or captured photos will be added. Continue anyway?')) {
return;
}
}
// Add to current slideshow
const slideshow = savedSlideshows[currentSlideshowName];
if (!slideshow.directories) {
slideshow.directories = [];
}
// Add directories (check for duplicates)
let addedCount = 0;
selectedDirectories.forEach(newDir => {
const exists = slideshow.directories.find(existingDir =>
(typeof existingDir === 'string' ? existingDir : existingDir.path) === newDir.path
);
if (!exists) {
slideshow.directories.push(newDir);
addedCount++;
}
});
// Update captured photos setting only if it changed
const currentCapturedPhotos = slideshow.includeCapturedPhotos || false;
if (includeCapturedPhotos !== currentCapturedPhotos) {
slideshow.includeCapturedPhotos = includeCapturedPhotos;
}
// Save and update
slideshow.modified = Date.now();
saveSlideshowsToStorage();
updateSlideshowDetails();
closeLibraryDialog();
// Better messaging
let message = '';
if (addedCount > 0) {
message = `Added ${addedCount} director${addedCount === 1 ? 'y' : 'ies'} to slideshow!`;
} else if (selectedDirectories.length > 0) {
message = 'No new directories were added (duplicates found)';
} else {
message = 'No directories selected.';
}
if (includeCapturedPhotos !== currentCapturedPhotos) {
const capturedAction = includeCapturedPhotos ? 'enabled' : 'disabled';
message += ` Captured photos ${capturedAction}.`;
}
if (message.trim()) {
alert(message);
}
console.log('📚 Added from library:', { directories: addedCount, capturedPhotos: includeCapturedPhotos });
}
function closeLibraryDialog() {
const dialog = document.getElementById('library-selection-dialog');
if (dialog) dialog.remove();
}
// Background Management Functions
function setupColorControl(colorId) {
const colorPicker = document.getElementById(colorId);
const hexInput = document.getElementById(colorId + 'Hex');
colorPicker.addEventListener('change', (e) => {
const color = e.target.value;
hexInput.value = color;
currentSettings[colorId] = color;
applyBackground();
saveHypnoGallerySettings();
});
hexInput.addEventListener('input', (e) => {
const color = e.target.value;
if (/^#[0-9A-F]{6}$/i.test(color)) {
colorPicker.value = color;
currentSettings[colorId] = color;
applyBackground();
saveHypnoGallerySettings();
}
});
}
function updateBackgroundOptions(type) {
const solidOptions = document.getElementById('solidColorOptions');
const gradientOptions = document.getElementById('gradientOptions');
const blurredOptions = document.getElementById('blurredOptions');
// Hide all options first
solidOptions.style.display = 'none';
gradientOptions.style.display = 'none';
blurredOptions.style.display = 'none';
// Show relevant options
switch (type) {
case 'solid':
solidOptions.style.display = 'flex';
break;
case 'gradient':
gradientOptions.style.display = 'block';
break;
case 'blurred':
blurredOptions.style.display = 'block';
break;
}
}
function applyBackground() {
const gridContainer = document.getElementById('gridContainer');
if (!gridContainer) return;
const { backgroundType, backgroundColor, gradientColor1, gradientColor2, gradientDirection, blurAmount, blurOpacity } = currentSettings;
// Ensure container has proper positioning for backgrounds
if (gridContainer.style.position !== 'relative') {
gridContainer.style.position = 'relative';
}
// Remove existing background elements
const existingBg = gridContainer.querySelector('.slideshow-background');
if (existingBg) existingBg.remove();
// Reset any previous background styles
gridContainer.style.background = '';
gridContainer.style.backgroundColor = '';
switch (backgroundType) {
case 'solid':
gridContainer.style.backgroundColor = backgroundColor;
break;
case 'gradient':
const gradientBg = `linear-gradient(${gradientDirection}, ${gradientColor1}, ${gradientColor2})`;
gridContainer.style.background = gradientBg;
break;
case 'blurred':
gridContainer.style.backgroundColor = '#000000'; // Fallback color
createBlurredBackground();
break;
}
}
function createBlurredBackground() {
const gridContainer = document.getElementById('gridContainer');
// Use the first grid cell's image for the blur effect
const firstCell = gridCells.length > 0 ? gridCells[0] : null;
const currentImg = firstCell ? firstCell.element.querySelector('.grid-image') : null;
if (!currentImg || !currentImg.src || !gridContainer) {
console.log('🫧 Cannot create blurred background:', {
hasContainer: !!gridContainer,
hasImg: !!currentImg,
hasSrc: !!currentImg?.src
});
return;
}
console.log('🫧 Creating blurred background with:', currentImg.src);
// Create background element
const backgroundDiv = document.createElement('div');
backgroundDiv.className = 'slideshow-background';
backgroundDiv.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url("${currentImg.src}");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
filter: blur(${currentSettings.blurAmount}px);
opacity: ${currentSettings.blurOpacity / 100};
z-index: 1;
pointer-events: none;
`;
// Insert background behind the grid
gridContainer.insertBefore(backgroundDiv, gridContainer.firstChild);
// Ensure grid cells are above the background
gridCells.forEach(cell => {
if (cell.element) {
cell.element.style.position = 'relative';
cell.element.style.zIndex = '2';
}
});
console.log('✅ Blurred background created successfully');
}
function deleteSlideshow() {
if (!currentSlideshowName || !savedSlideshows[currentSlideshowName]) {
alert('Please load a slideshow first');
return;
}
const name = currentSlideshowName;
if (!confirm(`Delete slideshow "${name}"? This cannot be undone.`)) {
return;
}
delete savedSlideshows[name];
saveSlideshowsToStorage();
updateSlideshowsList();
// Clear current slideshow since it was deleted
currentSlideshowName = null;
updateCurrentSessionDisplay();
console.log('🗑️ Deleted slideshow:', name);
alert(`Slideshow "${name}" deleted successfully!`);
}
function updateUIFromSettings() {
// Timing settings
document.getElementById('timingMode').value = currentSettings.timingMode;
document.getElementById('durationSlider').value = currentSettings.duration;
document.getElementById('minDurationSlider').value = currentSettings.minDuration;
document.getElementById('maxDurationSlider').value = currentSettings.maxDuration;
document.getElementById('waveMinSlider').value = currentSettings.waveMin;
document.getElementById('waveMaxSlider').value = currentSettings.waveMax;
document.getElementById('waveRateSlider').value = currentSettings.waveRate;
// Transition settings
document.getElementById('transitionType').value = currentSettings.transitionType;
document.getElementById('transitionDuration').value = currentSettings.transitionDuration;
// Directory settings (checkbox removed)
// Background settings
document.getElementById('backgroundType').value = currentSettings.backgroundType;
document.getElementById('backgroundColor').value = currentSettings.backgroundColor;
document.getElementById('backgroundColorHex').value = currentSettings.backgroundColor;
document.getElementById('gradientColor1').value = currentSettings.gradientColor1;
document.getElementById('gradientColor1Hex').value = currentSettings.gradientColor1;
document.getElementById('gradientColor2').value = currentSettings.gradientColor2;
document.getElementById('gradientColor2Hex').value = currentSettings.gradientColor2;
document.getElementById('gradientDirection').value = currentSettings.gradientDirection;
document.getElementById('blurAmount').value = currentSettings.blurAmount;
document.getElementById('blurAmountValue').textContent = currentSettings.blurAmount + 'px';
document.getElementById('blurOpacity').value = currentSettings.blurOpacity;
document.getElementById('blurOpacityValue').textContent = currentSettings.blurOpacity + '%';
// Update displays
updateTimingSettings(currentSettings.timingMode);
updateTransitionSettings(currentSettings.transitionType);
updateBackgroundOptions(currentSettings.backgroundType);
// Refresh directory display (if function exists)
if (typeof updateLinkedDirectories === 'function') {
updateLinkedDirectories();
}
}
function updateCurrentSessionDisplay() {
let sourcesCount = 0;
if (currentSlideshowName && savedSlideshows[currentSlideshowName]) {
const slideshow = savedSlideshows[currentSlideshowName];
sourcesCount = (slideshow.directories || []).length + (slideshow.includeCapturedPhotos ? 1 : 0);
} else {
sourcesCount = currentSettings.selectedDirectories.length +
(currentSettings.includeCapturedPhotos ? 1 : 0);
}
document.getElementById('currentSources').textContent =
sourcesCount > 0 ? `${sourcesCount} sources selected` : 'No sources selected';
let timingText = currentSettings.timingMode;
if (currentSettings.timingMode === 'constant') {
timingText += ` (${(currentSettings.duration/1000).toFixed(1)}s)`;
} else if (currentSettings.timingMode === 'random') {
timingText += ` (${(currentSettings.minDuration/1000).toFixed(1)}-${(currentSettings.maxDuration/1000).toFixed(1)}s)`;
} else if (currentSettings.timingMode === 'wave') {
timingText += ` (${(currentSettings.waveMin/1000).toFixed(1)}-${(currentSettings.waveMax/1000).toFixed(1)}s)`;
}
document.getElementById('currentTiming').textContent = timingText;
document.getElementById('currentTransitions').textContent =
`${currentSettings.transitionType} (${(currentSettings.transitionDuration/1000).toFixed(1)}s)`;
}
// Slideshow Directory Management Functions
let selectedDirectoryIndex = -1;
function selectSlideshowDirectory(index) {
// Remove previous selection
document.querySelectorAll('.slideshow-directory-item').forEach(item => {
item.style.border = '1px solid transparent';
item.style.background = 'rgba(102, 126, 234, 0.1)';
});
// Select new item
const items = document.querySelectorAll('.slideshow-directory-item');
if (items[index]) {
items[index].style.border = '1px solid #667eea';
items[index].style.background = 'rgba(102, 126, 234, 0.3)';
selectedDirectoryIndex = index;
document.getElementById('removeDirectoryBtn').disabled = false;
}
}
async function addDirectoryToSlideshow() {
if (!currentSlideshowName || !savedSlideshows[currentSlideshowName]) {
alert('Please load a slideshow first');
return;
}
try {
// Check if we're in Electron environment
if (!window.electronAPI || !window.electronAPI.selectDirectory) {
alert('Directory selection is only available in the desktop version of the application.');
return;
}
// Use the existing selectDirectory method
const directoryPath = await window.electronAPI.selectDirectory();
if (directoryPath) {
const directoryName = directoryPath.split(/[\\/]/).pop();
// Initialize directories array if it doesn't exist
if (!savedSlideshows[currentSlideshowName].directories) {
savedSlideshows[currentSlideshowName].directories = [];
}
// Check if directory already exists
const existingDir = savedSlideshows[currentSlideshowName].directories.find(dir =>
(typeof dir === 'string' ? dir : dir.path) === directoryPath
);
if (existingDir) {
alert('This directory is already added to the slideshow');
return;
}
// Add directory to slideshow
savedSlideshows[currentSlideshowName].directories.push({
path: directoryPath,
name: directoryName
});
savedSlideshows[currentSlideshowName].modified = Date.now();
saveSlideshowsToStorage();
updateSlideshowDetails();
updateCurrentSessionDisplay();
console.log('📁 Added directory to slideshow:', directoryName);
alert(`Added directory: ${directoryName}`);
} else {
console.log('Directory selection cancelled');
}
} catch (error) {
console.error('❌ Error adding directory:', error);
alert('Error adding directory: ' + error.message);
}
}
function removeDirectoryFromSlideshow() {
if (!currentSlideshowName || !savedSlideshows[currentSlideshowName] || selectedDirectoryIndex === -1) {
return;
}
const directories = savedSlideshows[currentSlideshowName].directories || [];
if (selectedDirectoryIndex >= 0 && selectedDirectoryIndex < directories.length) {
const removedDir = directories[selectedDirectoryIndex];
directories.splice(selectedDirectoryIndex, 1);
savedSlideshows[currentSlideshowName].modified = Date.now();
saveSlideshowsToStorage();
selectedDirectoryIndex = -1;
updateSlideshowDetails();
updateCurrentSessionDisplay();
console.log('🗑️ Removed directory from slideshow:', removedDir.name || removedDir);
}
}
async function testSlideshowDirectories() {
if (!currentSlideshowName || !savedSlideshows[currentSlideshowName]) {
return;
}
const slideshow = savedSlideshows[currentSlideshowName];
const directories = slideshow.directories || [];
if (directories.length === 0) {
alert('No directories to test');
return;
}
let totalImages = 0;
let validDirs = 0;
let invalidDirs = [];
// Test each directory
for (const dir of directories) {
const dirPath = typeof dir === 'string' ? dir : dir.path;
const dirName = typeof dir === 'string' ? dirPath.split(/[\\/]/).pop() : dir.name;
try {
// For electron environments, we could use fs to check directory
// For now, just assume they're valid and count as 1 image each for demo
validDirs++;
totalImages += 10; // Placeholder count
} catch (error) {
invalidDirs.push(dirName);
}
}
const message = `
📁 Directory Test Results:
✅ Valid directories: ${validDirs}
❌ Invalid directories: ${invalidDirs.length}
🖼️ Estimated images: ${totalImages}
${invalidDirs.length > 0 ? `\nInvalid directories:\n${invalidDirs.join('\n')}` : ''}
`;
alert(message.trim());
}
async function getSlideshowImages() {
let images = [];
// If a slideshow is loaded, use its directories
if (currentSlideshowName && savedSlideshows[currentSlideshowName]) {
const slideshow = savedSlideshows[currentSlideshowName];
const directories = slideshow.directories || [];
console.log('📁 Loading images from slideshow directories:', directories.length);
// Load images from slideshow-specific directories
for (const dir of directories) {
const dirPath = typeof dir === 'string' ? dir : dir.path;
try {
const dirImages = await loadImagesFromDirectory(dirPath);
images.push(...dirImages);
console.log(`📁 Loaded ${dirImages.length} images from: ${dirPath}`);
} catch (error) {
console.error(`❌ Error loading directory ${dirPath}:`, error);
}
}
// Include captured photos if enabled
if (slideshow.includeCapturedPhotos) {
const capturedPhotos = getCapturedPhotos();
images.push(...capturedPhotos);
console.log(`📷 Added ${capturedPhotos.length} captured photos`);
}
} else {
// Fallback to main library if no slideshow selected
console.log('📚 Using main image library');
images = [...imageLibrary];
}
console.log(`🖼️ Total slideshow images: ${images.length}`);
return images;
}
// Grid-specific image loading function for individual cells
async function getGridCellImages(slideshow) {
let images = [];
console.log(`🔲 Loading images for grid cell slideshow: ${slideshow.name}`);
// Load images from slideshow-specific directories
const directories = slideshow.directories || [];
console.log(`📁 Slideshow directories: ${directories.length}`);
for (const dir of directories) {
const dirPath = typeof dir === 'string' ? dir : dir.path;
try {
const dirImages = await loadImagesFromDirectory(dirPath);
images.push(...dirImages);
console.log(`📁 Grid cell loaded ${dirImages.length} images from: ${dirPath}`);
} catch (error) {
console.error(`❌ Grid cell error loading directory ${dirPath}:`, error);
}
}
// Include captured photos if enabled
if (slideshow.includeCapturedPhotos) {
const capturedPhotos = getCapturedPhotos();
images.push(...capturedPhotos);
console.log(`📷 Grid cell added ${capturedPhotos.length} captured photos`);
}
// Fallback to main library if no directories configured
if (images.length === 0) {
console.log('📚 Grid cell using main image library as fallback');
images = [...imageLibrary];
}
console.log(`🖼️ Grid cell total images: ${images.length}`);
return images;
}
async function loadImagesFromDirectory(directoryPath) {
// Load images from a directory recursively using the main process function
console.log(`🔍 Starting recursive image scan of: ${directoryPath}`);
if (typeof window !== 'undefined' && window.electronAPI && window.electronAPI.readImageDirectoryRecursive) {
try {
// Use the proper main process recursive function
const files = await window.electronAPI.readImageDirectoryRecursive(directoryPath);
console.log(`✅ Found ${files.length} images recursively`);
// Convert to the expected format for the slideshow system
const processedFiles = files.map(file => ({
name: file.name,
fullPath: file.path,
isWebcamCapture: false,
source: 'slideshow-directory'
}));
return processedFiles;
} catch (error) {
console.error('❌ Error reading directory recursively:', error);
return [];
}
} else {
console.warn('⚠️ Directory loading not available in this environment');
return [];
}
}
function getCapturedPhotos() {
// Get captured photos from localStorage or game library
try {
const captured = localStorage.getItem('capturedPhotos');
if (captured) {
const photos = JSON.parse(captured);
return photos.map(photo => ({
name: photo.name || 'Captured Photo',
path: photo.dataURL || photo.path,
fullPath: photo.dataURL || photo.path,
isWebcamCapture: true,
source: 'captured-photos'
}));
}
} catch (error) {
console.error('❌ Error loading captured photos:', error);
}
return [];
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
console.log('🌀 Hypno Gallery DOM loaded');
// Initialize theme switcher UI
if (window.themeManager) {
const themeSwitcher = window.themeManager.createThemeToggle();
const container = document.getElementById('theme-switcher-container');
if (container) {
container.appendChild(themeSwitcher);
console.log('✅ Theme switcher initialized');
}
}
setTimeout(() => {
initializeHypnoGallery();
loadSavedSlideshows();
// Initialize grid layout with default mode (after slideshows are loaded)
setTimeout(() => {
const displayMode = document.getElementById('displayMode');
if (displayMode && displayMode.value) {
updateDisplayMode();
}
}, 100);
// Dropdown removed - slideshow loading now handled by dialog system
}, 1000);
});
// Initialize timing settings display
document.addEventListener('DOMContentLoaded', () => {
updateTimingSettings('constant');
updateTransitionSettings('fade');
});
</script>
</body>
</html>