updated library to be on its own page

This commit is contained in:
dilgenfritz 2025-11-26 06:15:40 -06:00
parent 8789b8e16b
commit cbdea7bf3b
16 changed files with 3925 additions and 562 deletions

Binary file not shown.

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
@ -192,7 +192,7 @@
<span class="feature-icon">🌀</span>
<span class="feature-text">Hypno Gallery</span>
</button>
<button class="hero-feature btn-feature" id="library-btn">
<button class="hero-feature btn-feature" id="library-btn" onclick="window.location.href='library.html'">
<span class="cassie-icon"></span>
<span class="feature-text">Library</span>
</button>
@ -883,259 +883,6 @@
</div>
</div>
<!-- Library Screen -->
<div id="library-screen" class="screen">
<h2>📚 Media Library</h2>
<p>Manage all your media content in one place</p>
<!-- Library Navigation Tabs -->
<div class="library-tabs">
<button class="library-tab active" data-tab="images">🖼️ Images</button>
<button class="library-tab" data-tab="audio">🎵 Audio</button>
<button class="library-tab" data-tab="video">🎬 Video</button>
<button class="library-tab" data-tab="gallery">📸 Gallery</button>
</div>
<!-- Images Tab Content -->
<div id="library-images-content" class="library-content active">
<div class="content-section">
<h3>🖼️ Image Library Management</h3>
<p>Link directories from your computer to access image content</p>
<!-- Directory Management Section -->
<div class="directory-management-section">
<h4>📁 Linked Image Directories</h4>
<div class="directory-controls">
<button id="lib-add-image-directory-btn" class="btn btn-primary">📁 Add Directory</button>
<button id="lib-add-individual-images-btn" class="btn btn-primary">🖼️ Add Individual Images</button>
<button id="lib-refresh-image-directories-btn" class="btn btn-secondary">🔄 Refresh</button>
<button id="lib-clear-image-directories-btn" class="btn btn-danger">🗑️ Clear All</button>
<span id="lib-directories-count">0 directories linked</span>
</div>
<div class="linked-directories-container">
<div id="linked-image-directories-list">
<div class="no-directories">No image directories linked yet</div>
</div>
</div>
</div>
<!-- Image Gallery -->
<div class="gallery-section">
<div class="gallery-header">
<h4>🖼️ Current Image Library</h4>
<div class="gallery-controls">
<select id="lib-image-category-filter">
<option value="all">All Images</option>
<option value="tasks">🎯 Task Images</option>
<option value="consequences">⚠️ Consequence Images</option>
<option value="rewards">🎁 Reward Images</option>
<option value="verification">📷 Verification Photos</option>
<option value="jpg">JPEG Images</option>
<option value="png">PNG Images</option>
<option value="gif">GIF Images</option>
</select>
<span id="lib-image-count">0 images</span>
</div>
</div>
<div class="image-gallery active" id="lib-image-gallery">
<div class="no-images-message">
<p><EFBFBD> No images found in linked directories</p>
<p>Click "Add Directory" to link a folder containing images</p>
</div>
</div>
</div>
</div>
</div>
<!-- Audio Tab Content -->
<div id="library-audio-content" class="library-content">
<div class="content-section">
<h3>🎵 Audio Library Management</h3>
<p>Organize your background music and ambient sounds</p>
<!-- Audio Upload Section -->
<div class="upload-section">
<h4>🎵 Import Audio Files</h4>
<div class="upload-controls">
<button id="lib-import-background-music-btn" class="btn btn-primary">🎵 Background Music</button>
<button id="lib-import-ambient-audio-btn" class="btn btn-secondary">🌊 Ambient Sounds</button>
<input type="file" id="lib-audio-upload-input" accept="audio/*" multiple style="display: none;">
</div>
<div class="upload-info desktop-feature">
<span>💻 Desktop: Native file dialogs • Supports MP3, WAV, OGG, M4A formats</span>
</div>
<div class="directory-controls">
<button id="lib-audio-storage-info-btn" class="btn btn-outline">📊 Storage Info</button>
<button id="lib-cleanup-invalid-audio-btn" class="btn btn-warning">🧹 Cleanup</button>
<button id="lib-clear-all-audio-btn" class="btn btn-danger">🗑️ Clear All</button>
</div>
</div>
<!-- Audio Library -->
<div class="audio-gallery-section">
<div class="gallery-header">
<h4>🎵 Current Audio Library</h4>
<div class="gallery-controls">
<select id="lib-audio-category-filter">
<option value="all">All Categories</option>
<option value="background">Background Music</option>
<option value="ambient">Ambient Sounds</option>
</select>
<span id="lib-audio-count">0 files</span>
</div>
</div>
<div class="audio-gallery" id="lib-audio-gallery">
<div class="no-audio-message">
<p>🎵 No audio files found</p>
<p>Import audio to get started</p>
</div>
</div>
</div>
</div>
</div>
<!-- Video Tab Content -->
<div id="library-video-content" class="library-content">
<div class="content-section">
<h3>🎬 Video Library Management</h3>
<p>Manage your video content for enhanced training sessions</p>
<!-- Video Directory Management Section -->
<div class="directory-management-section">
<h4>📁 Linked Video Directories</h4>
<p>Link directories from your computer to access video content</p>
<div class="directory-controls">
<button id="lib-add-video-directory-btn" class="btn btn-primary">📁 Add Directory</button>
<button id="lib-add-individual-videos-btn" class="btn btn-primary">🎬 Add Individual Videos</button>
<button id="lib-refresh-video-directories-btn" class="btn btn-secondary">🔄 Refresh</button>
<button id="lib-clear-video-directories-btn" class="btn btn-danger">🗑️ Clear All</button>
<span id="lib-video-directories-count">0 directories linked</span>
</div>
<div class="linked-directories-container">
<div id="linked-video-directories-list">
<div class="no-directories">No video directories linked yet</div>
</div>
</div>
</div>
<!-- Video Library -->
<div class="video-gallery-section">
<div class="gallery-header">
<h4>🎬 Current Video Library</h4>
<div class="gallery-controls">
<select id="lib-video-category-filter">
<option value="all">All Categories</option>
<option value="training">Training Videos</option>
<option value="background">Background Videos</option>
</select>
<span id="lib-video-count">0 files</span>
</div>
</div>
<div class="video-gallery active" id="lib-video-gallery">
<div class="no-video-message">
<p>🎬 No video files found</p>
<p>Import videos to get started</p>
</div>
</div>
</div>
</div>
</div>
<!-- Gallery Tab Content -->
<div id="library-gallery-content" class="library-content">
<div class="content-section">
<h3>📸 Photo Gallery</h3>
<p>Browse and organize your photo collections</p>
<!-- Gallery Categories -->
<div class="gallery-categories">
<button class="gallery-category-btn active" data-category="all">All Photos</button>
<button class="gallery-category-btn" data-category="dress-up">Dress Up</button>
<button class="gallery-category-btn" data-category="studio">Studio</button>
<button class="gallery-category-btn" data-category="custom">Custom</button>
</div>
<!-- Photo Gallery Display -->
<div class="photo-galleries">
<div id="lib-all-photos-gallery" class="photo-gallery active">
<div class="gallery-header">
<h4>📸 All Photos</h4>
<span class="photo-count" id="lib-all-photos-count">0 photos</span>
</div>
<div class="bulk-actions">
<div class="selection-controls">
<button id="select-all-photos" class="btn btn-small">☑️ Select All</button>
<button id="deselect-all-photos" class="btn btn-small">☐ Deselect All</button>
<span id="selected-count" class="selected-count">0 selected</span>
</div>
<div class="action-buttons">
<button id="download-selected-photos" class="btn btn-success" disabled>📥 Download Selected</button>
<button id="delete-selected-photos" class="btn btn-danger" disabled>🗑️ Delete Selected</button>
</div>
</div>
<div class="photo-grid" id="lib-all-photos-grid">
<div class="no-photos-message">
<p>📸 No photos found</p>
<p>Take some photos during gameplay to see them here</p>
</div>
</div>
</div>
<div id="lib-dress-up-photos-gallery" class="photo-gallery">
<div class="gallery-header">
<h4>👗 Dress Up Photos</h4>
<span class="photo-count" id="lib-dress-up-photos-count">0 photos</span>
</div>
<div class="bulk-actions">
<div class="selection-controls">
<button id="select-all-dress-up" class="btn btn-small">☑️ Select All</button>
<button id="deselect-all-dress-up" class="btn btn-small">☐ Deselect All</button>
<span id="selected-dress-up-count" class="selected-count">0 selected</span>
</div>
<div class="action-buttons">
<button id="download-selected-dress-up" class="btn btn-success" disabled>📥 Download Selected</button>
<button id="delete-selected-dress-up" class="btn btn-danger" disabled>🗑️ Delete Selected</button>
</div>
</div>
<div class="photo-grid" id="lib-dress-up-photos-grid">
<div class="no-photos-message">
<p>👗 No dress up photos found</p>
</div>
</div>
</div>
<div id="lib-studio-photos-gallery" class="photo-gallery">
<div class="gallery-header">
<h4>🎬 Studio Photos</h4>
<span class="photo-count" id="lib-studio-photos-count">0 photos</span>
</div>
<div class="photo-grid" id="lib-studio-photos-grid">
<div class="no-photos-message">
<p>🎬 No studio photos found</p>
</div>
</div>
</div>
<div id="lib-custom-photos-gallery" class="photo-gallery">
<div class="gallery-header">
<h4>⭐ Custom Photos</h4>
<span class="photo-count" id="lib-custom-photos-count">0 photos</span>
</div>
<div class="photo-grid" id="lib-custom-photos-grid">
<div class="no-photos-message">
<p>⭐ No custom photos found</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Library Controls -->
<div class="management-buttons">
<button id="back-to-start-from-library-btn" class="btn btn-secondary">Back to Start</button>
<button id="refresh-library-btn" class="btn btn-primary">🔄 Refresh Library</button>
</div>
</div>
</main>
</div>
@ -3747,28 +3494,13 @@
// Set up library button (only once)
const libraryBtn = document.getElementById('library-btn');
console.log('🔍 Library button found:', !!libraryBtn);
console.log('🔍 Library button has handler:', libraryBtn ? libraryBtn.hasAttribute('data-handler-attached') : 'button not found');
if (libraryBtn && !libraryBtn.hasAttribute('data-handler-attached')) {
console.log('🔧 Attaching library button handler...');
libraryBtn.setAttribute('data-handler-attached', 'true');
libraryBtn.addEventListener('click', () => {
console.log('📚 Library button clicked');
console.log('🎮 Game instance available:', !!window.game);
console.log('🔧 showScreen method available:', !!(window.game && typeof window.game.showScreen === 'function'));
if (window.game && typeof window.game.showScreen === 'function') {
console.log('📺 Showing library screen...');
window.game.showScreen('library-screen');
// Set up library tab handlers when screen is shown
setTimeout(() => {
console.log('⚙️ Setting up library handlers...');
setupLibraryHandlers();
}, 100);
} else {
console.error('Game instance not available for library');
console.error('Available game methods:', window.game ? Object.keys(window.game) : 'No game object');
}
console.log('📚 Library button clicked - navigating to library.html');
window.location.href = 'library.html';
});
console.log('✅ Library button handler attached successfully');
} else if (libraryBtn && libraryBtn.hasAttribute('data-handler-attached')) {
@ -3916,22 +3648,8 @@
if (libraryBtn) {
libraryBtn.onclick = function(e) {
e.preventDefault();
// Show library screen directly
const libraryScreen = document.getElementById('library-screen');
const startScreen = document.getElementById('start-screen');
if (libraryScreen && startScreen) {
startScreen.classList.remove('active');
libraryScreen.classList.add('active');
// Set up library handlers
setTimeout(() => {
setupLibraryHandlers();
}, 100);
} else {
console.error('Could not find library or start screen');
}
console.log('📚 Library button clicked - navigating to library.html');
window.location.href = 'library.html';
};
}
}

1000
library.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -239,91 +239,117 @@
<script>
// Initialize cinema when page loads
document.addEventListener('DOMContentLoaded', async function() {
console.log('🎬 Initializing Porn Cinema...');
try {
console.log('🎬 Initializing Porn Cinema...');
// 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');
}
}
// Initialize desktop file manager if in Electron environment
if (window.electronAPI) {
// Try to reuse existing desktop file manager from parent window first
if (window.opener && window.opener.game && window.opener.game.fileManager) {
console.log('🔗 Reusing desktop file manager from main window');
window.desktopFileManager = window.opener.game.fileManager;
} else {
console.log('🆕 Creating new desktop file manager instance');
// Create a minimal data manager for the cinema (since we don't have the full game instance)
const minimalDataManager = {
get: (key) => {
try {
return JSON.parse(localStorage.getItem(key));
} catch {
return null;
}
},
set: (key, value) => {
localStorage.setItem(key, JSON.stringify(value));
}
};
window.desktopFileManager = new DesktopFileManager(minimalDataManager);
}
console.log('🖥️ Desktop File Manager initialized for porn cinema');
// Wait for the desktop file manager to fully initialize
// This includes loading linked directories and video files
let retries = 0;
const maxRetries = 50; // Wait up to 5 seconds
while (retries < maxRetries) {
// Check if initialization is complete by verifying video directories are set up
if (window.desktopFileManager.videoDirectories &&
window.desktopFileManager.videoDirectories.background) {
console.log('✅ Desktop file manager video directories are ready');
break;
// 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');
}
// Waiting for desktop file manager to initialize...
await new Promise(resolve => setTimeout(resolve, 100));
retries++;
}
if (retries >= maxRetries) {
console.warn('⚠️ Desktop file manager took too long to initialize');
}
// Force refresh of linked directories to ensure we have the latest video data
try {
// If we're reusing the main window's file manager, don't reload/refresh
// Initialize desktop file manager if in Electron environment
if (window.electronAPI) {
// Try to reuse existing desktop file manager from parent window first
if (window.opener && window.opener.game && window.opener.game.fileManager) {
console.log('📁 Using directories from main window file manager');
console.log('🔗 Reusing desktop file manager from main window');
window.desktopFileManager = window.opener.game.fileManager;
} else {
// Only reload and refresh if we created a new instance
await window.desktopFileManager.loadLinkedDirectories();
console.log('🆕 Creating new desktop file manager instance');
// Create a minimal data manager for the cinema (since we don't have the full game instance)
const minimalDataManager = {
get: (key) => {
try {
return JSON.parse(localStorage.getItem(key));
} catch {
return null;
}
},
set: (key, value) => {
localStorage.setItem(key, JSON.stringify(value));
}
};
window.desktopFileManager = new DesktopFileManager(minimalDataManager);
}
} catch (error) {
console.warn('⚠️ Error refreshing directories:', error);
console.log('🖥️ Desktop File Manager initialized for porn cinema');
// Wait for the desktop file manager to fully initialize
// This includes loading linked directories and video files
let retries = 0;
const maxRetries = 50; // Wait up to 5 seconds
while (retries < maxRetries) {
// Check if initialization is complete by verifying video directories are set up
if (window.desktopFileManager.videoDirectories &&
window.desktopFileManager.videoDirectories.background) {
console.log('✅ Desktop file manager video directories are ready');
break;
}
// Waiting for desktop file manager to initialize...
await new Promise(resolve => setTimeout(resolve, 100));
retries++;
}
if (retries >= maxRetries) {
console.warn('⚠️ Desktop file manager took too long to initialize');
}
// Force refresh of linked directories to ensure we have the latest video data
try {
// If we're reusing the main window's file manager, don't reload/refresh
if (window.opener && window.opener.game && window.opener.game.fileManager) {
console.log('📁 Using directories from main window file manager');
} else {
// Only reload and refresh if we created a new instance
await window.desktopFileManager.loadLinkedDirectories();
}
} catch (error) {
console.warn('⚠️ Error refreshing directories:', error);
}
} else if (!window.electronAPI) {
console.warn('⚠️ Running in browser mode - video management limited');
}
} else if (!window.electronAPI) {
console.warn('⚠️ Running in browser mode - video management limited');
// Initialize the cinema after desktop file manager is ready
window.pornCinema = new PornCinema();
await window.pornCinema.initialize();
// Hide loading overlay
setTimeout(() => {
const loadingOverlay = document.getElementById('cinema-loading');
if (loadingOverlay) {
loadingOverlay.style.display = 'none';
}
}, 1000);
} catch (error) {
console.error('❌ Critical error initializing Porn Cinema:', error);
// Show error to user
const loadingOverlay = document.getElementById('cinema-loading');
if (loadingOverlay) {
loadingOverlay.innerHTML = `
<div class="loading-content">
<h2 style="color: #ff4444;">⚠️ Error Loading Cinema</h2>
<p>${error.message || 'Unknown error occurred'}</p>
<p style="font-size: 0.9em; opacity: 0.7;">Check console for details</p>
<button onclick="location.reload()" class="btn btn-primary" style="margin-top: 20px;">
🔄 Retry
</button>
<button onclick="location.href='index.html'" class="btn btn-secondary" style="margin-top: 10px;">
🏠 Return Home
</button>
</div>
`;
}
}
// Initialize the cinema after desktop file manager is ready
window.pornCinema = new PornCinema();
await window.pornCinema.initialize();
// Hide loading overlay
setTimeout(() => {
document.getElementById('cinema-loading').style.display = 'none';
}, 1000);
});
// Back to home functionality
@ -331,6 +357,20 @@
showExitConfirmationDialog();
});
// Cleanup on window close to prevent memory leaks
window.addEventListener('beforeunload', () => {
if (window.pornCinema && typeof window.pornCinema.destroy === 'function') {
window.pornCinema.destroy();
}
});
// Cleanup when navigating away
window.addEventListener('pagehide', () => {
if (window.pornCinema && typeof window.pornCinema.destroy === 'function') {
window.pornCinema.destroy();
}
});
function getSassyTheaterAttendantDialog() {
// Array of sassy theater attendant responses
const sassyResponses = [
@ -795,8 +835,15 @@
}
// Theater mode button event listeners
document.getElementById('theater-mode').addEventListener('click', toggleTheaterMode);
document.getElementById('theater-mode-btn').addEventListener('click', toggleTheaterMode);
const theaterModeBtn = document.getElementById('theater-mode');
const theaterModeBtnControl = document.getElementById('theater-mode-btn');
if (theaterModeBtn) {
theaterModeBtn.addEventListener('click', toggleTheaterMode);
}
if (theaterModeBtnControl) {
theaterModeBtnControl.addEventListener('click', toggleTheaterMode);
}
// Keyboard shortcut to toggle help
document.addEventListener('keydown', (e) => {

View File

@ -1293,8 +1293,6 @@
// Check MP4 codec support for Electron
function checkMP4Support() {
console.log('🎥 Checking MP4 codec support...');
const mp4Codecs = [
'video/mp4;codecs=avc1.42E01E',
'video/mp4;codecs=avc1.4D401E',
@ -1305,7 +1303,6 @@
const supportedCodecs = mp4Codecs.filter(codec => MediaRecorder.isTypeSupported(codec));
if (supportedCodecs.length > 0) {
console.log('✅ MP4 codec support confirmed:', supportedCodecs);
return true;
} else {
console.error('❌ No MP4 codecs supported. Available types:');
@ -1330,15 +1327,12 @@
// Initialize Quick Play when page loads
document.addEventListener('DOMContentLoaded', async function() {
console.log('⚡ Initializing Quick Play...');
// 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');
}
}
@ -1348,21 +1342,17 @@
// Load saved settings
await loadSavedSettings();
console.log('✅ Settings loaded');
// Setup event listeners
setupEventListeners();
console.log('✅ Event listeners setup');
// Initialize player stats
if (typeof PlayerStats !== 'undefined') {
window.playerStats = new PlayerStats();
console.log('✅ Player stats initialized');
}
// Initialize desktop file manager if in Electron environment
await initializeFileManager();
console.log('✅ File manager initialized');
// Add window unload handler with better cleanup
let unloadHandlerAdded = false;
@ -2267,12 +2257,10 @@
}
function updateRecordingOverlay() {
if (!recordingOverlayEnabled) {
console.log('🎬 updateRecordingOverlay called but recording not enabled');
return;
}
// Don't update if game is ending or not running
if (!isGameRunning || window.isEndingGame) return;
console.log('🎬 Updating recording overlay content');
if (!recordingOverlayEnabled) return;
// Try multiple selectors to find task elements
let taskText = document.getElementById('task-text');
@ -2368,7 +2356,9 @@
// Call this function whenever task content changes
function syncTaskWithOverlay() {
console.log('🎬 syncTaskWithOverlay called, recordingOverlayEnabled:', recordingOverlayEnabled);
// Don't run if game is ending or not running
if (!isGameRunning || window.isEndingGame) return;
if (recordingOverlayEnabled) {
updateRecordingOverlay();
@ -2939,6 +2929,46 @@
window.desktopFileManager = new DesktopFileManager(minimalDataManager);
console.log('🖥️ Desktop File Manager initialized for Quick Play');
// Track saved photo IDs to prevent duplicates
const savedPhotoIds = new Set();
// Intercept localStorage.setItem to save photos to file system
const originalSetItem = localStorage.setItem.bind(localStorage);
localStorage.setItem = function(key, value) {
// Call original first
originalSetItem(key, value);
// If it's a photo being saved, also save to file system
if ((key === 'capturedPhotos' || key === 'verificationPhotos') && window.desktopFileManager?.isElectron) {
try {
const photos = JSON.parse(value);
if (Array.isArray(photos) && photos.length > 0) {
const lastPhoto = photos[photos.length - 1];
if (lastPhoto.data) {
// Create unique ID based on timestamp and first 20 chars of data
const photoId = `${lastPhoto.timestamp}_${lastPhoto.data.substring(0, 20)}`;
// Only save if we haven't saved this photo yet
if (!savedPhotoIds.has(photoId)) {
savedPhotoIds.add(photoId);
// Save to file system asynchronously
window.desktopFileManager.savePhoto(lastPhoto.data, 'quick-play')
.then(photoData => {
if (photoData) {
console.log('📸 Intercepted and saved photo to file system:', photoData.filename);
}
})
.catch(err => console.error('📸 Failed to intercept save:', err));
}
}
}
} catch (err) {
// Ignore parsing errors
}
}
};
}
}
@ -2965,7 +2995,6 @@
skipped: 0,
xp: 0
};
console.log('📊 Session stats reset:', sessionStats);
// Start periodic XP display updates (every minute)
if (window.xpDisplayInterval) {
@ -3590,11 +3619,17 @@
}
function startGameTimer(timeLimit) {
// Clear any existing timer first
if (window.gameTimerInterval) {
clearInterval(window.gameTimerInterval);
window.gameTimerInterval = null;
}
// Handle endless mode
if (timeLimit === -1) {
console.log('🔄 Starting endless session - no time limit');
let timeElapsed = 0;
const timerInterval = setInterval(() => {
window.gameTimerInterval = setInterval(() => {
if (!isGameRunning || (gameInstance && gameInstance.gameState && gameInstance.gameState.isPaused)) {
return;
}
@ -3607,7 +3642,7 @@
// Handle timed mode
let timeLeft = timeLimit;
const timerInterval = setInterval(() => {
window.gameTimerInterval = setInterval(() => {
if (!isGameRunning || (gameInstance && gameInstance.gameState && gameInstance.gameState.isPaused)) {
return;
}
@ -3616,7 +3651,8 @@
updateGameStatus({ timer: formatTime(timeLeft) });
if (timeLeft <= 0) {
clearInterval(timerInterval);
clearInterval(window.gameTimerInterval);
window.gameTimerInterval = null;
endGame();
}
}, 1000);
@ -3708,7 +3744,6 @@
taskTags.includes(includeTag)
);
});
console.log(`🏷️ Filtered to ${filteredTasks.length} tasks with include tags:`, quickPlaySettings.includeTags);
}
// Step 2: Remove any tasks that have exclude tags (this overrides include)
@ -3719,12 +3754,10 @@
taskTags.includes(excludeTag)
);
});
console.log(`🚫 Filtered out tasks with exclude tags:`, quickPlaySettings.excludeTags, `- ${filteredTasks.length} tasks remaining`);
}
if (filteredTasks.length > 0) {
const randomTask = filteredTasks[Math.floor(Math.random() * filteredTasks.length)];
console.log('📋 Loading filtered task:', randomTask.text || randomTask.description, 'Tags:', randomTask.tags);
displayTask(randomTask);
} else {
@ -3756,6 +3789,15 @@
function quickPlayCompleteTask(task) {
console.log('✅ Quick Play task completed:', task.text);
// Clear inactivity timeout when completing task
if (inactivityTimeout) {
clearTimeout(inactivityTimeout);
inactivityTimeout = null;
}
// Reset pause state
isTimerPaused = false;
// Check timer validation
if (!taskCompleteAllowed) {
console.log('⏱️ Task completion blocked - timer not finished');
@ -4163,9 +4205,187 @@
}
// Global timer variables
let taskTimer = null;
window.taskTimer = null;
let taskTimeRemaining = 0;
let taskCompleteAllowed = false;
let inactivityTimeout = null;
let isTimerPaused = false;
let pausedTimeRemaining = 0;
const timeoutMessages = [
"Hello? Did you fall asleep, slut? 🥱",
"Still there, or did you chicken out? 🐔",
"Getting bored already? Typical... 💅",
"Did you get distracted, or are you just slow? 🤔",
"Knock knock... anyone home? 🚪",
"Are you still playing or did you give up? 😏",
"Time to wake up, sleeping beauty! ⏰",
"Lost interest already? How disappointing... 😒",
"Did you forget what you were doing? 🤦",
"Still with us, or did reality call? 📱",
"Zoned out? That's not very obedient... 😈",
"Are we having fun yet? ...Anyone? 🙄",
"Did you wander off, or are you just thinking really hard? 🧠",
"Helloooo? Earth to slut! 🌍",
"Taking a break without permission? Naughty... 😤"
];
function startInactivityTimeout() {
// Clear any existing timeout
if (inactivityTimeout) {
clearTimeout(inactivityTimeout);
}
// Start 30-second timeout
inactivityTimeout = setTimeout(() => {
if (taskTimeRemaining <= 0 && !isTimerPaused) {
// Timer has hit 0 but task wasn't completed
pauseTimer();
showTimeoutDialog();
}
}, 30000); // 30 seconds
}
function pauseTimer() {
if (window.taskTimer && !isTimerPaused) {
clearInterval(window.taskTimer);
isTimerPaused = true;
pausedTimeRemaining = taskTimeRemaining;
console.log('⏸️ Timer paused due to inactivity');
}
}
function resumeTimer(timerElement, completeButton) {
if (isTimerPaused) {
isTimerPaused = false;
taskTimeRemaining = pausedTimeRemaining;
// Restart the timer interval
window.taskTimer = setInterval(() => {
try {
taskTimeRemaining--;
updateTimerDisplay(timerElement);
if (taskTimeRemaining <= 0) {
clearInterval(window.taskTimer);
window.taskTimer = null;
taskCompleteAllowed = true;
const currentCompleteBtn = document.getElementById('complete-task');
if (currentCompleteBtn) {
currentCompleteBtn.disabled = false;
currentCompleteBtn.textContent = 'Complete Task';
currentCompleteBtn.style.opacity = '1';
}
if (timerElement) {
timerElement.style.color = '#00ff00';
timerElement.textContent = 'COMPLETE!';
setTimeout(() => {
timerElement.style.color = '';
timerElement.textContent = '0:00';
}, 2000);
}
// Start inactivity timeout after timer completes
startInactivityTimeout();
}
} catch (error) {
console.error('🔧 Error in timer callback:', error);
}
}, 1000);
console.log('▶️ Timer resumed');
}
}
function showTimeoutDialog() {
const message = timeoutMessages[Math.floor(Math.random() * timeoutMessages.length)];
const dialog = document.createElement('div');
dialog.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 100000;
animation: fadeIn 0.3s ease;
`;
dialog.innerHTML = `
<style>
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
</style>
<div style="
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px 60px;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
text-align: center;
max-width: 500px;
animation: pulse 2s ease-in-out infinite;
">
<div style="
font-size: 64px;
margin-bottom: 20px;
">⏰</div>
<h2 style="
color: white;
font-size: 28px;
margin-bottom: 20px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
">${message}</h2>
<p style="
color: rgba(255, 255, 255, 0.9);
font-size: 18px;
margin-bottom: 30px;
">You've been idle for 30 seconds.<br>Click below to continue your session.</p>
<button id="continue-session-btn" style="
background: white;
color: #667eea;
border: none;
padding: 15px 40px;
font-size: 20px;
font-weight: bold;
border-radius: 10px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transition: transform 0.2s ease;
" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
Yes, I'm Still Here! 👋
</button>
</div>
`;
document.body.appendChild(dialog);
// Add click handler
const continueBtn = dialog.querySelector('#continue-session-btn');
continueBtn.addEventListener('click', () => {
dialog.remove();
const timerElement = document.getElementById('task-timer');
const completeButton = document.getElementById('complete-task');
resumeTimer(timerElement, completeButton);
// Restart inactivity timeout
if (taskTimeRemaining <= 0) {
startInactivityTimeout();
}
});
}
function startTaskTimer(durationMinutes, timerElement, completeButton) {
// console.log('🔧 startTaskTimer called with:', {
@ -4177,8 +4397,8 @@
// });
// Clear any existing timer
if (taskTimer) {
clearInterval(taskTimer);
if (window.taskTimer) {
clearInterval(window.taskTimer);
// console.log('🔧 Cleared existing timer');
}
@ -4203,7 +4423,7 @@
// Start countdown
console.log('🔧 About to create timer interval...');
try {
taskTimer = setInterval(() => {
window.taskTimer = setInterval(() => {
try {
// console.log('🔧 Timer callback fired, timeRemaining:', taskTimeRemaining);
taskTimeRemaining--;
@ -4216,7 +4436,8 @@
// Check if timer completed
if (taskTimeRemaining <= 0) {
clearInterval(taskTimer);
clearInterval(window.taskTimer);
window.taskTimer = null;
taskCompleteAllowed = true;
console.log('✅ Task timer completed!');
@ -4242,13 +4463,16 @@
}
console.log('✅ Task timer completed - task can now be completed');
// Start inactivity timeout - will trigger after 30 seconds if user doesn't complete task
startInactivityTimeout();
}
} catch (error) {
console.error('🔧 Error in timer callback:', error);
}
}, 1000);
console.log('🔧 Timer interval created with ID:', taskTimer);
console.log('🔧 Timer interval created with ID:', window.taskTimer);
// Test if the timer is actually working by checking in 2 seconds
setTimeout(() => {
@ -4261,6 +4485,9 @@
}
function updateTimerDisplay(timerElement) {
// Don't update if game is ending or not running
if (!isGameRunning || window.isEndingGame) return;
// console.log('🔧 updateTimerDisplay called with timeRemaining:', taskTimeRemaining, 'element:', !!timerElement);
if (!timerElement) return;
@ -4292,9 +4519,10 @@
}
function stopTaskTimer() {
if (taskTimer) {
clearInterval(taskTimer);
taskTimer = null;
if (window.taskTimer) {
clearInterval(window.taskTimer);
window.taskTimer = null;
console.log('✅ Task timer stopped and cleared');
}
taskTimeRemaining = 0;
taskCompleteAllowed = true;
@ -4308,66 +4536,143 @@
}
function skipTask(task) {
// Clear inactivity timeout when skipping
if (inactivityTimeout) {
clearTimeout(inactivityTimeout);
inactivityTimeout = null;
}
// Reset pause state
isTimerPaused = false;
// Load consequence or next task
loadNextTask();
}
function endGame() {
console.log('endGame called - starting game termination');
console.log('🛑 ========== END GAME CALLED ==========');
try {
// Clear XP display update interval
if (window.xpDisplayInterval) {
clearInterval(window.xpDisplayInterval);
window.xpDisplayInterval = null;
console.log('🔄 Cleared XP display update interval');
}
// Immediately mute all videos to stop audio instantly
const allVideos = document.querySelectorAll('video');
allVideos.forEach(video => {
video.muted = true;
video.pause();
});
console.log(`Immediately muted and paused ${allVideos.length} video elements`);
// Immediately set flag to prevent multiple calls
if (window.isEndingGame) {
console.log('Game already ending, ignoring duplicate call');
console.log('⚠️ Game already ending, ignoring duplicate call');
return;
}
window.isEndingGame = true;
console.log('Setting isGameRunning to false');
console.log('🛑 Setting isGameRunning to false');
isGameRunning = false;
// Clear ALL timers and intervals immediately - MOST IMPORTANT
console.log('🛑 Clearing all timers and intervals...');
// Clear game timer interval
if (window.gameTimerInterval) {
console.log('🛑 Clearing gameTimerInterval ID:', window.gameTimerInterval);
clearInterval(window.gameTimerInterval);
window.gameTimerInterval = null;
}
// Clear XP display interval
if (window.xpDisplayInterval) {
console.log('🛑 Clearing xpDisplayInterval ID:', window.xpDisplayInterval);
clearInterval(window.xpDisplayInterval);
window.xpDisplayInterval = null;
}
// Clear recording timer interval
if (window.recordingTimerInterval) {
console.log('🛑 Clearing recordingTimerInterval ID:', window.recordingTimerInterval);
clearInterval(window.recordingTimerInterval);
window.recordingTimerInterval = null;
}
// Clear task timer - THIS IS CRITICAL
if (window.taskTimer) {
console.log('🛑 Clearing taskTimer ID:', window.taskTimer);
clearInterval(window.taskTimer);
window.taskTimer = null;
}
// Clear inactivity timeout
if (inactivityTimeout) {
console.log('🛑 Clearing inactivityTimeout');
clearTimeout(inactivityTimeout);
inactivityTimeout = null;
}
// Reset pause state
isTimerPaused = false;
taskTimeRemaining = 0;
// Clear any task-related timers from game instance
if (gameInstance) {
if (gameInstance.taskTimer) {
console.log('🛑 Clearing gameInstance.taskTimer');
clearInterval(gameInstance.taskTimer);
gameInstance.taskTimer = null;
}
if (gameInstance.gameTimer) {
console.log('🛑 Clearing gameInstance.gameTimer');
clearInterval(gameInstance.gameTimer);
gameInstance.gameTimer = null;
}
}
// IMMEDIATELY STOP ALL VIDEOS - MOST IMPORTANT FOR USER EXPERIENCE
console.log('🛑 Stopping all videos...');
const allVideos = document.querySelectorAll('video');
console.log(`🛑 Found ${allVideos.length} video elements`);
allVideos.forEach((video, index) => {
console.log(`🛑 Stopping video ${index + 1}:`, video.id || 'no-id', video.src?.substring(0, 50));
video.pause();
video.muted = true;
video.currentTime = 0;
video.src = '';
// Remove source elements too
const sources = video.querySelectorAll('source');
sources.forEach(source => source.src = '');
});
// Stop flash message system
stopFlashMessageSystem();
// Stop all task loading and selection
currentTask = null;
// Stop the task timer (redundant but safe)
stopTaskTimer();
// Clear any pending task loads
if (window.nextTaskTimeout) {
clearTimeout(window.nextTaskTimeout);
window.nextTaskTimeout = null;
}
// Clean up game instance
if (gameInstance) {
console.log('Cleaning up game instance');
try {
// Stop any task presentation
if (gameInstance.currentTask) {
gameInstance.currentTask = null;
}
if (gameInstance.pauseGame) {
gameInstance.pauseGame();
console.log('Game paused');
}
if (gameInstance.cleanup) {
gameInstance.cleanup();
console.log('Game cleanup called');
}
if (gameInstance.audioManager) {
gameInstance.audioManager.stopAll();
console.log('Audio stopped');
}
} catch (cleanupError) {
console.warn('Error during game cleanup:', cleanupError);
console.error('❌ Error during game cleanup:', cleanupError);
}
}
// Clean up background video (check if it exists first)
if (typeof backgroundVideoPlayer !== 'undefined' && backgroundVideoPlayer) {
console.log('Stopping background video via backgroundVideoPlayer');
try {
backgroundVideoPlayer.pause();
if (backgroundVideoPlayer.destroy) {
@ -4375,85 +4680,37 @@
}
backgroundVideoPlayer = null;
} catch (videoError) {
console.warn('Error stopping background video via backgroundVideoPlayer:', videoError);
console.error('❌ Error stopping background video via backgroundVideoPlayer:', videoError);
}
} else {
console.log('No backgroundVideoPlayer found - checking for background video element directly');
}
// Quick Play specific background video cleanup
// Quick Play specific background video cleanup (already done above but being thorough)
const backgroundVideo = document.getElementById('background-video');
if (backgroundVideo) {
console.log('Found Quick Play background video - stopping it');
try {
backgroundVideo.pause();
backgroundVideo.currentTime = 0;
backgroundVideo.src = '';
backgroundVideo.load(); // Force reload to clear
const source = backgroundVideo.querySelector('source');
if (source) {
source.src = '';
}
console.log('✅ Quick Play background video stopped and cleared');
} catch (bgVideoError) {
console.warn('Error stopping Quick Play background video:', bgVideoError);
}
} else {
console.log('No Quick Play background video element found');
}
// Also clean up the background video container
const backgroundVideoContainer = document.getElementById('background-video-container');
if (backgroundVideoContainer) {
console.log('Clearing background video container');
try {
backgroundVideoContainer.innerHTML = '<div style="opacity: 0;">Video stopped</div>';
console.log('✅ Background video container cleared');
} catch (containerError) {
console.warn('Error clearing background video container:', containerError);
console.error('❌ Error stopping Quick Play background video:', bgVideoError);
}
}
// Clean up video player manager - THIS IS THE IMPORTANT ONE
// Clean up video player manager
if (window.videoPlayerManager) {
console.log('Stopping all videos via VideoPlayerManager');
try {
window.videoPlayerManager.stopAllVideos();
console.log('All videos stopped via VideoPlayerManager');
} catch (videoManagerError) {
console.warn('Error stopping videos via VideoPlayerManager:', videoManagerError);
console.error('❌ Error stopping videos via VideoPlayerManager:', videoManagerError);
}
} else {
console.log('VideoPlayerManager not found - trying manual video cleanup');
}
// Manual fallback - find and stop all video elements (do this regardless)
try {
const allVideos = document.querySelectorAll('video');
console.log(`Found ${allVideos.length} video elements to stop`);
allVideos.forEach((video, index) => {
const videoId = video.id || 'unknown';
const videoSrc = video.src || video.currentSrc || 'no source';
console.log(`Stopping video element ${index}: ID="${videoId}", src="${videoSrc}"`);
try {
video.pause();
video.currentTime = 0;
video.muted = true; // Mute first to stop audio immediately
video.src = '';
// Remove source elements too
const sources = video.querySelectorAll('source');
sources.forEach(source => source.src = '');
video.load(); // Force reload to clear the video
console.log(`✅ Stopped video ${index} (${videoId})`);
} catch (singleVideoError) {
console.warn(`❌ Error stopping video ${index} (${videoId}):`, singleVideoError);
}
});
console.log(`✅ Manual video cleanup complete - processed ${allVideos.length} videos`);
} catch (manualError) {
console.warn('❌ Error in manual video cleanup:', manualError);
}
console.log('✅ All timers cleared and videos stopped');
// Clean up periodic popups and any active popups
if (gameInstance && gameInstance.popupImageManager) {
@ -5449,7 +5706,26 @@
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `quick-play-session-${timestamp}.${extension}`;
// Try to save to user-selected directory first (if available)
// Try to save to file system first (Electron)
if (window.desktopFileManager && window.desktopFileManager.isElectron) {
try {
const videoData = await window.desktopFileManager.saveVideo(blob, 'quick-play', extension);
if (videoData) {
console.log('✅ Session recording saved to file system:', videoData.filename);
if (window.flashMessageManager) {
window.flashMessageManager.show(`📹 Recording saved: ${videoData.filename}`, 'positive');
}
recordedChunks = [];
return;
}
} catch (error) {
console.error('❌ File system save failed, trying directory handle:', error);
}
}
// Try to save to user-selected directory (if available)
if (quickPlaySettings.webcamDirectoryHandle) {
try {
const fileHandle = await quickPlaySettings.webcamDirectoryHandle.getFileHandle(filename, { create: true });

56
reset-to-fresh.bat Normal file
View File

@ -0,0 +1,56 @@
@echo off
REM ==============================================================================
REM Reset Gooner Training Academy to Fresh Install
REM Windows Batch Script
REM ==============================================================================
echo.
echo ========================================================================
echo Gooner Training Academy - Fresh Install Reset
echo ========================================================================
echo.
echo This will DELETE ALL local data including settings and progress.
echo This action CANNOT be undone!
echo.
choice /C YN /M "Are you sure you want to continue"
if errorlevel 2 goto :cancel
if errorlevel 1 goto :proceed
:cancel
echo.
echo Reset cancelled. No changes were made.
pause
exit /b 0
:proceed
echo.
echo Starting reset process...
echo.
REM Close any running Electron instances
echo [1/2] Closing running instances...
taskkill /F /IM electron.exe 2>nul
timeout /t 2 /nobreak >nul
REM Clear Electron userData directory (this is where localStorage persists)
echo [2/2] Clearing Electron user data...
set USERDATA_PATH=%APPDATA%\webGame
if exist "%USERDATA_PATH%" (
echo Deleting: %USERDATA_PATH%
rmdir /S /Q "%USERDATA_PATH%"
echo Deleted Electron user data directory
) else (
echo User data directory not found (this is normal for first run)
)
echo.
echo ========================================================================
echo Reset Complete!
echo ========================================================================
echo.
echo All settings and progress have been cleared.
echo Next time you run 'npm start', it will be like a fresh install.
echo.
pause
exit /b 0

67
reset-to-fresh.sh Normal file
View File

@ -0,0 +1,67 @@
#!/bin/bash
# ==============================================================================
# Reset Gooner Training Academy to Fresh Install
# macOS/Linux Shell Script
# ==============================================================================
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo ""
echo "========================================================================"
echo " Gooner Training Academy - Fresh Install Reset"
echo "========================================================================"
echo ""
echo -e "${YELLOW}This will DELETE ALL local data including settings and progress.${NC}"
echo -e "${RED}This action CANNOT be undone!${NC}"
echo ""
# Prompt for confirmation
read -p "Are you sure you want to continue? (yes/no): " -r
echo ""
if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]
then
echo -e "${GREEN}Reset cancelled. No changes were made.${NC}"
exit 0
fi
echo "Starting reset process..."
echo ""
# Close any running Electron instances
echo "[1/2] Closing running instances..."
pkill -f "electron" 2>/dev/null
pkill -f "Electron" 2>/dev/null
sleep 2
# Clear Electron userData directory
echo "[2/2] Clearing Electron user data..."
# Determine the userData path based on OS
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
USERDATA_PATH="$HOME/Library/Application Support/webGame"
else
# Linux
USERDATA_PATH="$HOME/.config/webGame"
fi
if [ -d "$USERDATA_PATH" ]; then
echo "Deleting: $USERDATA_PATH"
rm -rf "$USERDATA_PATH"
echo "Deleted Electron user data directory"
else
echo "User data directory not found (this is normal for first run)"
fi
echo ""
echo "========================================================================"
echo -e "${GREEN} Reset Complete!${NC}"
echo "========================================================================"
echo ""
echo "All settings and progress have been cleared."
echo "Next time you run 'npm start', it will be like a fresh install."
echo ""

303
reset.html Normal file
View File

@ -0,0 +1,303 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reset to Fresh Install - Gooner Training Academy</title>
<link rel="stylesheet" href="src/styles/color-variables.css">
<link rel="stylesheet" href="src/styles/styles.css">
<style>
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: var(--bg-primary);
color: var(--text-primary);
padding: 20px;
}
.reset-container {
max-width: 600px;
background: var(--bg-secondary);
border: 2px solid var(--color-primary);
border-radius: 10px;
padding: 40px;
text-align: center;
}
.reset-container h1 {
color: var(--color-primary);
margin-bottom: 20px;
}
.warning {
background: rgba(255, 59, 48, 0.1);
border: 2px solid #ff3b30;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
color: #ff3b30;
font-weight: bold;
}
.data-list {
text-align: left;
background: var(--bg-primary);
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.data-list ul {
margin: 10px 0;
padding-left: 20px;
}
.data-list li {
margin: 8px 0;
}
.button-group {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 30px;
}
.btn {
padding: 15px 30px;
font-size: 16px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
}
.btn-danger {
background: #ff3b30;
color: white;
}
.btn-danger:hover {
background: #ff1f14;
transform: scale(1.05);
}
.btn-secondary {
background: var(--bg-primary);
color: var(--text-primary);
border: 2px solid var(--color-primary);
}
.btn-secondary:hover {
background: var(--bg-primary-overlay-10);
transform: scale(1.05);
}
.status-message {
margin-top: 20px;
padding: 15px;
border-radius: 8px;
display: none;
}
.status-message.success {
background: rgba(52, 199, 89, 0.1);
border: 2px solid #34c759;
color: #34c759;
display: block;
}
.status-message.error {
background: rgba(255, 59, 48, 0.1);
border: 2px solid #ff3b30;
color: #ff3b30;
display: block;
}
.countdown {
font-size: 48px;
color: var(--color-primary);
margin: 20px 0;
}
</style>
</head>
<body>
<div class="reset-container">
<h1>🔄 Reset to Fresh Install</h1>
<div class="warning">
⚠️ IMPORTANT: You must use the script files to reset!
</div>
<div class="data-list">
<p><strong>Electron stores data persistently - clearing it here won't work!</strong></p>
<p style="margin-top: 15px;">To properly reset the application:</p>
<ol style="text-align: left; margin-top: 10px;">
<li><strong>Close this application completely</strong></li>
<li><strong>Windows:</strong> Run <code>reset-to-fresh.bat</code></li>
<li><strong>Mac/Linux:</strong> Run <code>./reset-to-fresh.sh</code></li>
</ol>
<hr style="margin: 20px 0; border-color: var(--color-primary);">
<p><strong>The script will delete ALL of the following:</strong></p>
<ul>
<li>Player stats and progress</li>
<li>Custom tasks and consequences</li>
<li>Saved Quick Play settings and presets</li>
<li>Flash messages and popup image settings</li>
<li>Theme preferences</li>
<li>Linked image directories</li>
<li>Linked video directories</li>
<li>Linked audio directories</li>
<li>Captured photos</li>
<li>Verification photos</li>
<li>All localStorage data</li>
<li>All IndexedDB data</li>
</ul>
<p style="color: var(--color-primary); margin-top: 15px;">
<strong>NOTE:</strong> Your actual media files will NOT be deleted.
Only the links to them will be cleared.
</p>
</div>
<div class="button-group">
<button id="reset-btn" class="btn btn-danger">Reset to Fresh Install</button>
<button id="cancel-btn" class="btn btn-secondary">Cancel</button>
</div>
<div id="status-message" class="status-message"></div>
<div id="countdown" class="countdown" style="display: none;"></div>
</div>
<script>
const resetBtn = document.getElementById('reset-btn');
const cancelBtn = document.getElementById('cancel-btn');
const statusMessage = document.getElementById('status-message');
const countdownElement = document.getElementById('countdown');
cancelBtn.addEventListener('click', () => {
window.location.href = 'index.html';
});
resetBtn.addEventListener('click', async () => {
// Double confirmation
const confirmed = confirm('Are you absolutely sure you want to delete ALL your data?\n\nThis action CANNOT be undone!');
if (!confirmed) return;
try {
resetBtn.disabled = true;
cancelBtn.disabled = true;
// Clear all localStorage
console.log('Clearing localStorage...');
const itemsCleared = [];
// List of known localStorage keys to clear
const keysToRemove = [
'playerStats',
'gameProgress',
'quickPlaySettings',
'linkedImageDirectories',
'linkedVideoDirectories',
'linkedIndividualVideos',
'linkedIndividualImages',
'linkedAudioDirectories',
'capturedPhotos',
'verificationPhotos',
'unifiedVideoLibrary',
'videoLibrary',
'videoFiles',
'customTasks',
'disabledTasks',
'flashMessages',
'flashMessageConfig',
'popupImageConfig',
'themePreference',
'audioSettings',
'webcamRecordingDirectory',
'webcamDirectoryHandleId',
'taskData',
'consequenceData',
'customTags',
'sessionHistory',
'achievements'
];
// Remove specific keys
keysToRemove.forEach(key => {
if (localStorage.getItem(key) !== null) {
localStorage.removeItem(key);
itemsCleared.push(key);
}
});
// Also clear any remaining keys (in case there are others)
const allKeys = Object.keys(localStorage);
allKeys.forEach(key => {
if (!itemsCleared.includes(key)) {
localStorage.removeItem(key);
itemsCleared.push(key);
}
});
console.log(`Cleared ${itemsCleared.length} localStorage items:`, itemsCleared);
// Clear all IndexedDB databases
console.log('Clearing IndexedDB...');
const databases = await indexedDB.databases();
const dbPromises = databases.map(db => {
return new Promise((resolve) => {
console.log(`Deleting IndexedDB: ${db.name}`);
const request = indexedDB.deleteDatabase(db.name);
request.onsuccess = () => {
console.log(`✅ Deleted IndexedDB: ${db.name}`);
resolve();
};
request.onerror = () => {
console.warn(`Failed to delete IndexedDB: ${db.name}`);
resolve();
};
});
});
await Promise.all(dbPromises);
// Clear sessionStorage too
console.log('Clearing sessionStorage...');
sessionStorage.clear();
// Show success message
statusMessage.className = 'status-message success';
statusMessage.textContent = `✅ Success! Cleared ${itemsCleared.length} localStorage items and ${databases.length} databases.`;
// Show countdown and redirect
countdownElement.style.display = 'block';
let countdown = 3;
countdownElement.textContent = countdown;
const countdownInterval = setInterval(() => {
countdown--;
if (countdown > 0) {
countdownElement.textContent = countdown;
} else {
clearInterval(countdownInterval);
countdownElement.textContent = 'Redirecting...';
setTimeout(() => {
window.location.href = 'index.html';
}, 500);
}
}, 1000);
} catch (error) {
console.error('Error during reset:', error);
statusMessage.className = 'status-message error';
statusMessage.textContent = `❌ Error: ${error.message}`;
resetBtn.disabled = false;
cancelBtn.disabled = false;
}
});
</script>
</body>
</html>

View File

@ -5,7 +5,7 @@ echo =================================================
echo.
:: Set distribution info
set DIST_NAME=Gooner-Training-Academy-v4.0-Beta
set DIST_NAME=Gooner-Training-Academy-v4.1-Beta
set BUILD_DATE=%DATE:~-4,4%-%DATE:~-10,2%-%DATE:~-7,2%
set OUTPUT_DIR=..\%DIST_NAME%

View File

@ -330,6 +330,24 @@ ipcMain.handle('delete-file', async (event, filePath) => {
}
});
// Save base64 image to file
ipcMain.handle('save-base64-image', async (event, filePath, base64Data) => {
try {
// Ensure the directory exists
await fs.mkdir(path.dirname(filePath), { recursive: true });
// Convert base64 to buffer and write to file
const buffer = Buffer.from(base64Data, 'base64');
await fs.writeFile(filePath, buffer);
console.log(`📸 Saved image: ${filePath}`);
return true;
} catch (error) {
console.error('Error saving base64 image:', error);
return false;
}
});
// Audio-specific IPC handlers
ipcMain.handle('select-audio', async () => {
const result = await dialog.showOpenDialog(mainWindow, {

View File

@ -28,6 +28,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
fileExists: (filePath) => ipcRenderer.invoke('file-exists', filePath),
createDirectory: (dirPath) => ipcRenderer.invoke('create-directory', dirPath),
deleteFile: (filePath) => ipcRenderer.invoke('delete-file', filePath),
saveBase64Image: (filePath, base64Data) => ipcRenderer.invoke('save-base64-image', filePath, base64Data),
// Platform info
platform: process.platform,

Binary file not shown.

View File

@ -2854,7 +2854,8 @@ body.theme-monochrome {
#lib-video-gallery .video-info {
padding: 8px 6px !important; /* Increased padding for more space */
background: rgba(0, 0, 0, 0.9) !important; /* Darker background for better contrast */
height: 75px !important; /* Increased height for better text display */
height: auto !important; /* Auto height to fit content */
min-height: 75px !important; /* Minimum height for consistency */
overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
@ -2862,7 +2863,15 @@ body.theme-monochrome {
border-top: 1px solid rgba(255, 255, 255, 0.2) !important;
}
#lib-video-gallery .video-name {
#lib-video-gallery .video-details {
display: flex !important;
flex-direction: column !important;
gap: 4px !important;
width: 100% !important;
}
#lib-video-gallery .video-name,
#lib-video-gallery .video-title {
font-size: 12px !important; /* Slightly larger font for better visibility */
color: #ffffff !important; /* Bright white for visibility */
margin: 0 0 3px 0 !important; /* Increased bottom margin */
@ -2873,7 +2882,8 @@ body.theme-monochrome {
white-space: nowrap !important;
}
#lib-video-gallery .video-directory {
#lib-video-gallery .video-directory,
#lib-video-gallery .video-meta {
font-size: 10px !important; /* Slightly larger for better readability */
color: #e0e0e0 !important; /* Brighter light gray for better visibility */
opacity: 1 !important; /* Remove opacity to ensure visibility */
@ -2881,7 +2891,14 @@ body.theme-monochrome {
text-overflow: ellipsis !important;
white-space: nowrap !important;
line-height: 1.1 !important;
margin: 0 !important;
margin: 0 0 4px 0 !important;
}
#lib-video-gallery .btn-small {
font-size: 10px !important;
padding: 4px 8px !important;
margin-top: 0 !important;
}
}
/* Override conflicting gallery item styles for video items - COMPACT LAYOUT */
@ -3098,7 +3115,8 @@ body.theme-monochrome {
padding: var(--space-sm);
}
.video-name {
.video-name,
.video-title {
font-weight: 600;
font-size: var(--font-sm);
color: var(--text-primary);
@ -6085,19 +6103,42 @@ button#start-mirror-btn:disabled {
#lib-video-gallery .video-info {
padding: 10px !important;
background: var(--bg-card) !important;
display: flex !important;
flex-direction: column !important;
}
#lib-video-gallery .video-name {
#lib-video-gallery .video-details {
display: flex !important;
flex-direction: column !important;
gap: 4px !important;
width: 100% !important;
}
#lib-video-gallery .video-name,
#lib-video-gallery .video-title {
font-size: var(--font-sm) !important;
color: var(--text-primary) !important;
margin-bottom: 4px !important;
font-weight: 500 !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
#lib-video-gallery .video-directory {
#lib-video-gallery .video-directory,
#lib-video-gallery .video-meta {
font-size: var(--font-xs) !important;
color: var(--text-secondary) !important;
opacity: 0.7 !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
#lib-video-gallery .btn-small {
font-size: var(--font-xs) !important;
padding: 4px 8px !important;
margin-top: 4px !important;
}
/* ALSO APPLY ALL VIDEO STYLING TO UNIFIED-VIDEO-GALLERY */

View File

@ -17,6 +17,8 @@ class DesktopFileManager {
rewards: null,
punishments: null
};
this.photoDirectory = null; // Directory for captured photos
this.videoRecordingDirectory = null; // Directory for recorded videos
// External video directories (linked, not copied)
this.externalVideoDirectories = []; // Array of linked directory objects
@ -44,6 +46,9 @@ class DesktopFileManager {
this.videoDirectories.rewards = await window.electronAPI.pathJoin(this.appPath, 'videos', 'rewards');
this.videoDirectories.punishments = await window.electronAPI.pathJoin(this.appPath, 'videos', 'punishments');
this.photoDirectory = await window.electronAPI.pathJoin(this.appPath, 'photos', 'captured');
this.videoRecordingDirectory = await window.electronAPI.pathJoin(this.appPath, 'videos', 'recorded');
// Ensure directories exist
await window.electronAPI.createDirectory(this.imageDirectories.tasks);
await window.electronAPI.createDirectory(this.imageDirectories.consequences);
@ -51,6 +56,9 @@ class DesktopFileManager {
await window.electronAPI.createDirectory(this.audioDirectories.background);
await window.electronAPI.createDirectory(this.audioDirectories.ambient);
await window.electronAPI.createDirectory(this.photoDirectory);
await window.electronAPI.createDirectory(this.videoRecordingDirectory);
// Note: No longer creating/using local video directories
// All videos come from external linked directories only
@ -58,6 +66,8 @@ class DesktopFileManager {
console.log('App path:', this.appPath);
console.log('Image directories:', this.imageDirectories);
console.log('Audio directories:', this.audioDirectories);
console.log('Photo directory:', this.photoDirectory);
console.log('Video recording directory:', this.videoRecordingDirectory);
// Load any previously linked external directories
await this.loadLinkedDirectories();
@ -968,6 +978,193 @@ class DesktopFileManager {
.replace(/\b\w/g, l => l.toUpperCase()); // Capitalize first letters
}
/**
* Save a captured photo to the file system
* @param {string} dataURL - Base64 data URL of the photo
* @param {string} sessionType - Type of session (e.g., 'training-academy', 'dress-up')
* @returns {Promise<Object|null>} Photo metadata or null if failed
*/
async savePhoto(dataURL, sessionType = 'training-academy') {
if (!this.isElectron) {
console.warn('📸 Photo saving only available in desktop version');
return null;
}
if (!this.photoDirectory) {
console.error('📸 Photo directory not initialized yet');
return null;
}
try {
console.log('📸 Attempting to save photo...');
const timestamp = Date.now();
const filename = `${sessionType}_${timestamp}.jpg`;
const filePath = await window.electronAPI.pathJoin(this.photoDirectory, filename);
console.log('📸 Photo will be saved to:', filePath);
// Convert data URL to base64 string (remove the "data:image/jpeg;base64," prefix)
const base64Data = dataURL.replace(/^data:image\/\w+;base64,/, '');
console.log('📸 Base64 data length:', base64Data.length);
// Save the file
const success = await window.electronAPI.saveBase64Image(filePath, base64Data);
console.log('📸 Save result:', success);
if (success) {
const photoData = {
filename: filename,
path: filePath,
fullPath: filePath,
isWebcamCapture: true,
timestamp: timestamp,
sessionType: sessionType,
url: `file:///${filePath.replace(/\\/g, '/')}`
};
console.log(`📸 Photo saved successfully: ${filename}`);
return photoData;
} else {
console.error('📸 Failed to save photo to file system - saveBase64Image returned false');
return null;
}
} catch (error) {
console.error('📸 Error saving photo:', error);
return null;
}
}
/**
* Load all captured photos from the photo directory
* @returns {Promise<Array>} Array of photo metadata objects
*/
async loadCapturedPhotos() {
if (!this.isElectron || !this.photoDirectory) {
return [];
}
try {
const files = await window.electronAPI.readDirectory(this.photoDirectory);
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif'];
const photoFiles = files.filter(file => {
const ext = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
return imageExtensions.includes(ext);
});
const photos = photoFiles.map(file => {
// Extract session type from filename (e.g., "training-academy_1234567890.jpg")
const parts = file.name.split('_');
const sessionType = parts.length > 1 ? parts.slice(0, -1).join('_') : 'unknown';
const timestamp = parts.length > 1 ? parseInt(parts[parts.length - 1].split('.')[0]) : Date.now();
return {
filename: file.name,
path: file.path,
fullPath: file.path,
isWebcamCapture: true,
timestamp: timestamp,
sessionType: sessionType,
url: `file:///${file.path.replace(/\\/g, '/')}`
};
});
console.log(`📸 Loaded ${photos.length} photos from file system`);
return photos;
} catch (error) {
console.error('Error loading captured photos:', error);
return [];
}
}
/**
* Delete a captured photo from the file system
* @param {string} filePath - Full path to the photo file
* @returns {Promise<boolean>} Success status
*/
async deletePhoto(filePath) {
if (!this.isElectron) {
return false;
}
try {
const success = await window.electronAPI.deleteFile(filePath);
if (success) {
console.log(`📸 Photo deleted: ${filePath}`);
return true;
}
return false;
} catch (error) {
console.error('Error deleting photo:', error);
return false;
}
}
/**
* Save a recorded video to the file system
* @param {Blob} videoBlob - Video blob data
* @param {string} sessionType - Type of session (e.g., 'quick-play', 'training-academy')
* @param {string} extension - File extension (e.g., 'mp4', 'webm')
* @returns {Promise<Object|null>} Video metadata or null if failed
*/
async saveVideo(videoBlob, sessionType = 'quick-play', extension = 'mp4') {
if (!this.isElectron) {
console.warn('📹 Video saving only available in desktop version');
return null;
}
if (!this.videoRecordingDirectory) {
console.error('📹 Video directory not initialized yet');
return null;
}
try {
console.log('📹 Attempting to save video...');
const timestamp = Date.now();
const filename = `${sessionType}_${timestamp}.${extension}`;
const filePath = await window.electronAPI.pathJoin(this.videoRecordingDirectory, filename);
console.log('📹 Video will be saved to:', filePath);
// Convert blob to ArrayBuffer then to base64
const arrayBuffer = await videoBlob.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const binaryString = uint8Array.reduce((data, byte) => data + String.fromCharCode(byte), '');
const base64Data = btoa(binaryString);
console.log('📹 Video data size:', videoBlob.size, 'bytes');
// Save the file using the same handler
const success = await window.electronAPI.saveBase64Image(filePath, base64Data);
console.log('📹 Save result:', success);
if (success) {
const videoData = {
filename: filename,
path: filePath,
fullPath: filePath,
timestamp: timestamp,
sessionType: sessionType,
size: videoBlob.size,
extension: extension,
url: `file:///${filePath.replace(/\\/g, '/')}`
};
console.log(`📹 Video saved successfully: ${filename}`);
return videoData;
} else {
console.error('📹 Failed to save video to file system');
return null;
}
} catch (error) {
console.error('📹 Error saving video:', error);
return null;
}
}
getVideoPath(videoName, category = 'background') {
if (!this.isElectron) {
return `videos/${videoName}`;

1586
src/utils/libraryManager.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1993,55 +1993,32 @@
console.log(`📸 Total photos loaded: ${totalPhotos} (${linkedPhotoDirectories.length} directories + ${linkedIndividualImages.length} individual images)`);
// Load previously captured webcam photos from localStorage
// Load previously captured photos from file system or localStorage
try {
const savedWebcamPhotos = JSON.parse(localStorage.getItem('trainingAcademyPhotos') || '[]');
if (savedWebcamPhotos.length > 0) {
trainingPhotoLibrary.push(...savedWebcamPhotos);
console.log(`📸 Loaded ${savedWebcamPhotos.length} previously captured photos from localStorage`);
}
// Also load main captured photos (including verification photos)
const mainCapturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
console.log('📸 Raw main captured photos from localStorage:', mainCapturedPhotos.length);
console.log('📸 Main captured photos sample:', mainCapturedPhotos.slice(-2));
if (mainCapturedPhotos.length > 0) {
// Convert main photos to library format and add them
let addedCount = 0;
mainCapturedPhotos.forEach(photo => {
if (!trainingPhotoLibrary.some(existing => existing.id === photo.id)) {
const libraryPhoto = {
id: photo.id || Date.now().toString(),
path: photo.data, // Base64 data
filename: photo.filename || `captured_${photo.timestamp}.jpg`,
timestamp: photo.timestamp,
isWebcamCapture: true,
type: photo.type || 'webcam_capture',
phase: photo.phase,
message: photo.message
};
trainingPhotoLibrary.push(libraryPhoto);
addedCount++;
}
});
console.log(`📸 Added ${addedCount} main captured photos to training library (${mainCapturedPhotos.length} total available)`);
console.log('📸 Training photo library now has:', trainingPhotoLibrary.length, 'photos');
console.log('📸 Verification photos in library:', trainingPhotoLibrary.filter(p => p.type === 'position_verification').length);
// Try loading from file system first (Electron)
if (window.desktopFileManager && window.desktopFileManager.isElectron) {
const filePhotos = await window.desktopFileManager.loadCapturedPhotos();
if (filePhotos.length > 0) {
trainingPhotoLibrary.push(...filePhotos);
console.log(`📸 Loaded ${filePhotos.length} photos from file system`);
}
} else {
// Browser fallback - load from localStorage
const savedWebcamPhotos = JSON.parse(localStorage.getItem('trainingAcademyPhotos') || '[]');
if (savedWebcamPhotos.length > 0) {
trainingPhotoLibrary.push(...savedWebcamPhotos);
console.log(`📸 Loaded ${savedWebcamPhotos.length} previously captured photos from localStorage`);
}
}
} catch (error) {
console.warn('⚠️ Failed to load saved webcam photos:', error);
console.warn('⚠️ Failed to load saved photos:', error);
}
document.getElementById('photoLibraryStatus').innerHTML =
`<span style="color: var(--color-success);">✅ ${trainingPhotoLibrary.length} photos available</span>`;
// Check for verification photos as well
const verificationPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
const mainCapturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
// Show gallery button if there are photos or verification photos
const totalPhotoCount = trainingPhotoLibrary.length + verificationPhotos.length;
// Show gallery button if there are photos
const totalPhotoCount = trainingPhotoLibrary.length;
if (totalPhotoCount > 0) {
document.getElementById('view-gallery-btn').style.display = 'inline-block';
@ -4717,6 +4694,66 @@
document.addEventListener('DOMContentLoaded', () => {
console.log('🎓 Training Academy DOM loaded');
// Initialize desktop file manager if in Electron environment
if (window.electronAPI && typeof DesktopFileManager !== 'undefined') {
// Create a simple data manager for the file manager
const simpleDataManager = {
get: (key) => {
try {
return JSON.parse(localStorage.getItem(key));
} catch {
return null;
}
},
set: (key, value) => {
localStorage.setItem(key, JSON.stringify(value));
}
};
window.desktopFileManager = new DesktopFileManager(simpleDataManager);
console.log('🖥️ Desktop File Manager initialized for training academy');
// Track saved photo IDs to prevent duplicates
const savedPhotoIds = new Set();
// Intercept localStorage.setItem to save photos to file system
const originalSetItem = localStorage.setItem.bind(localStorage);
localStorage.setItem = function(key, value) {
// Call original first
originalSetItem(key, value);
// If it's a photo being saved, also save to file system
if ((key === 'capturedPhotos' || key === 'verificationPhotos') && window.desktopFileManager?.isElectron) {
try {
const photos = JSON.parse(value);
if (Array.isArray(photos) && photos.length > 0) {
const lastPhoto = photos[photos.length - 1];
if (lastPhoto.data) {
// Create unique ID based on timestamp and first 20 chars of data
const photoId = `${lastPhoto.timestamp}_${lastPhoto.data.substring(0, 20)}`;
// Only save if we haven't saved this photo yet
if (!savedPhotoIds.has(photoId)) {
savedPhotoIds.add(photoId);
// Save to file system asynchronously
window.desktopFileManager.savePhoto(lastPhoto.data, 'training-academy')
.then(photoData => {
if (photoData) {
console.log('📸 Intercepted and saved photo to file system:', photoData.filename);
}
})
.catch(err => console.error('📸 Failed to intercept save:', err));
}
}
}
} catch (err) {
// Ignore parsing errors
}
}
};
}
// Initialize theme switcher UI
if (window.themeManager) {
const themeSwitcher = window.themeManager.createThemeToggle();
@ -4857,7 +4894,7 @@
if (photosNeededEl) photosNeededEl.textContent = photosNeeded;
}
function completePhotoSession() {
async function completePhotoSession() {
// Prevent multiple calls
if (!photoSessionActive) {
console.log('📸 Photo session already completed, skipping');
@ -4880,7 +4917,7 @@
// Add captured photos to training photo library
if (capturedPhotos.length > 0) {
addCapturedPhotosToLibrary(capturedPhotos);
await addCapturedPhotosToLibrary(capturedPhotos);
}
// Show completion message
@ -4902,51 +4939,64 @@
}
// Add captured photos to the training photo library
function addCapturedPhotosToLibrary(capturedPhotos) {
async function addCapturedPhotosToLibrary(capturedPhotos) {
console.log('📸 addCapturedPhotosToLibrary called with', capturedPhotos.length, 'photos');
console.log('📸 desktopFileManager available:', !!window.desktopFileManager);
console.log('📸 isElectron:', window.desktopFileManager?.isElectron);
const newPhotos = [];
// Get existing photos from localStorage
const existingPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
capturedPhotos.forEach((photo, index) => {
for (const [index, photo] of capturedPhotos.entries()) {
if (photo.dataURL) {
const photoData = {
filename: `captured_photo_${Date.now()}_${index}.jpg`,
path: photo.dataURL, // Use data URL as path for webcam photos
fullPath: photo.dataURL,
isWebcamCapture: true,
timestamp: photo.timestamp || Date.now(),
sessionType: photo.sessionType || 'dress-up-session',
imageData: photo.dataURL // This is what the main gallery expects
};
console.log(`📸 Processing photo ${index + 1}/${capturedPhotos.length}`);
// Save photo to file system if in Electron
let photoData;
if (window.desktopFileManager && window.desktopFileManager.isElectron) {
console.log('📸 Saving to file system...');
photoData = await window.desktopFileManager.savePhoto(photo.dataURL, 'training-academy');
if (!photoData) {
console.error('📸 Failed to save photo to file system - using fallback');
// Fallback to data URL
photoData = {
filename: `captured_photo_${Date.now()}_${index}.jpg`,
path: photo.dataURL,
fullPath: photo.dataURL,
isWebcamCapture: true,
timestamp: photo.timestamp || Date.now(),
sessionType: 'training-academy',
imageData: photo.dataURL
};
}
} else {
console.log('📸 Using browser fallback (data URL)');
// Browser fallback - use data URL
photoData = {
filename: `captured_photo_${Date.now()}_${index}.jpg`,
path: photo.dataURL,
fullPath: photo.dataURL,
isWebcamCapture: true,
timestamp: photo.timestamp || Date.now(),
sessionType: 'training-academy',
imageData: photo.dataURL
};
}
newPhotos.push(photoData);
trainingPhotoLibrary.push(photoData);
existingPhotos.push(photoData); // Add to localStorage format
} else {
console.warn('📸 Photo missing dataURL:', photo);
}
});
}
// Save updated photos to localStorage for main gallery
localStorage.setItem('capturedPhotos', JSON.stringify(existingPhotos));
console.log(`📸 Added ${newPhotos.length} photos to training library and localStorage`);
console.log(`📸 Total photos in localStorage: ${existingPhotos.length}`);
console.log(`📸 Added ${newPhotos.length} photos to training library`);
// Update photo library status
const statusEl = document.getElementById('photoLibraryStatus');
if (statusEl) {
statusEl.innerHTML = `<span style="color: var(--color-success);">✅ ${trainingPhotoLibrary.length} photos available (${newPhotos.length} newly captured)</span>`;
}
// Save to localStorage for persistence
try {
const existingPhotos = JSON.parse(localStorage.getItem('trainingAcademyPhotos') || '[]');
existingPhotos.push(...newPhotos);
localStorage.setItem('trainingAcademyPhotos', JSON.stringify(existingPhotos));
console.log('📸 Photos saved to localStorage for persistence');
} catch (error) {
console.warn('⚠️ Failed to save photos to localStorage:', error);
}
}
// Show captured photos in a gallery view
@ -4962,14 +5012,17 @@
<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: center;">
<h2 style="color: white; margin-bottom: 20px;">📸 Captured Photos Gallery</h2>
<div style="display: flex; flex-wrap: wrap; gap: 15px; max-width: 90%; max-height: 70%; overflow-y: auto; justify-content: center;">
${webcamPhotos.map((photo, index) => `
${webcamPhotos.map((photo, index) => {
// Use file URL if available, otherwise fallback to path (data URL for browser mode)
const imageSrc = photo.url || photo.path;
return `
<div style="border: 2px solid var(--text-white); border-radius: 10px; overflow: hidden;">
<img src="${photo.path}" style="width: 200px; height: 150px; object-fit: cover;" alt="Captured photo ${index + 1}">
<img src="${imageSrc}" style="width: 200px; height: 150px; object-fit: cover;" alt="Captured photo ${index + 1}">
<div style="background: rgba(0,0,0,0.8); color: white; padding: 5px; text-align: center; font-size: 12px;">
Photo ${index + 1} - ${new Date(photo.timestamp).toLocaleTimeString()}
</div>
</div>
`).join('')}
`}).join('')}
</div>
<button onclick="this.parentElement.remove()" style="margin-top: 20px; padding: 10px 20px; background: var(--color-danger); color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 16px;">
❌ Close Gallery