Complete media library cleanup - consolidate duplicate gallery systems

Phase 1 - Video Library Consolidation:
- Removed unified-video-gallery, loadUnifiedVideoGallery(), loadStandardVideoGallery(), loadLargeVideoGallery()
- Removed loadVideoGalleryContent() and all category-video-gallery references
- Removed setupVideoItemHandlers(), selectAllVideos(), deleteSelectedVideos(), deleteVideo()
- Consolidated to single lib-video-gallery system

 Phase 2 - Image Library Consolidation:
- Removed task-images-gallery, consequence-images-gallery elements and tabs
- Removed obsolete image selection buttons
- Added task/consequence/reward filtering to lib-image-gallery
- Implemented directory-based category filtering

 Phase 3 - Audio Library Consolidation:
- Removed background-audio-gallery, ambient-audio-gallery elements and tabs
- Removed obsolete audio selection buttons
- Enhanced lib-audio-gallery with background/ambient category filtering
- Implemented efficient audio filtering system

 Impact: ~500+ lines removed, 12+ duplicate functions eliminated, 9+ obsolete UI elements cleaned
 Result: Single consolidated library system per media type with enhanced filtering capabilities
This commit is contained in:
dilgenfritz 2025-11-11 20:41:53 -06:00
parent f1132822bf
commit 37ec5f0f1e
8 changed files with 1017 additions and 1081 deletions

View File

@ -18,9 +18,12 @@ The webGame uses an XP-based progression system where players advance through le
- 1 XP for tasks completed in <5 minutes
- 2 XP for tasks completed in 5-10 minutes
- 3 XP for tasks completed in >10 minutes
- **Session Time Bonus**: Additional XP for longer play sessions
- **Session Time Bonus**: Additional 2 XP for every 15 minutes of continuous play. no partial time awarded.
- **Session Recording Time Bonus**: Additional 1 XP for every 15 minutes of continuous play when the session is being recorded. no partial time awarded.
- **Incomplete Tasks**: Players still earn XP even for incomplete tasks
- **Example**: Completing a 20-minute task earns 3 XP = 3 XP total
- **Time bonus example**: A 45-minute quick play session earns 6 XP (3 x 15-minute intervals) = 6 XP total
- **Recording bonus example**: A 45-minute recorded quick play session earns an additional 3 XP + session time bonus is an additional 6 XP (3 x 15-minute intervals) = 9 XP total
### 🎭 Scenario Games
- **Base Time XP**: 1 XP per 2 minutes of gameplay
- **Webcam Bonus**: 1 XP per minute during webcam activities (x2 multiplier)

1076
index.html

File diff suppressed because it is too large Load Diff

28
package-lock.json generated
View File

@ -1,15 +1,15 @@
{
"name": "task-challenge-game",
"name": "gooner-training-academy",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "task-challenge-game",
"name": "gooner-training-academy",
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"electron": "^27.0.0",
"electron": "^39.1.1",
"electron-builder": "^24.6.4"
}
},
@ -590,13 +590,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "18.19.127",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.127.tgz",
"integrity": "sha512-gSjxjrnKXML/yo0BO099uPixMqfpJU0TKYjpfLU7TrtA2WWDki412Np/RSTPRil1saKBhvVVKzVx/p/6p94nVA==",
"version": "22.19.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.0.tgz",
"integrity": "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
"undici-types": "~6.21.0"
}
},
"node_modules/@types/plist": {
@ -1798,15 +1798,15 @@
}
},
"node_modules/electron": {
"version": "27.3.11",
"resolved": "https://registry.npmjs.org/electron/-/electron-27.3.11.tgz",
"integrity": "sha512-E1SiyEoI8iW5LW/MigCr7tJuQe7+0105UjqY7FkmCD12e2O6vtUbQ0j05HaBh2YgvkcEVgvQ2A8suIq5b5m6Gw==",
"version": "39.1.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-39.1.1.tgz",
"integrity": "sha512-VuFEI1yQ7BH3RYI5VZtwFlzGp4rpPRd5oEc26ZQIItVLcLTbXt4/O7o4hs+1fyg9Q3NvGAifgX5Vp5EBOIFpAg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^18.11.18",
"@types/node": "^22.7.7",
"extract-zip": "^2.0.1"
},
"bin": {
@ -3871,9 +3871,9 @@
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},

View File

@ -9,12 +9,13 @@
"build-win": "electron-builder --win",
"build-mac": "electron-builder --mac",
"build-linux": "electron-builder --linux",
"dev": "electron . --dev"
"dev": "electron . --dev",
"test-codecs": "electron . --dev --enable-logging"
},
"author": "Gooner Training Academy Developer",
"license": "MIT",
"devDependencies": {
"electron": "^27.0.0",
"electron": "^39.1.1",
"electron-builder": "^24.6.4"
},
"build": {
@ -60,4 +61,4 @@
"electron",
"nsfw"
]
}
}

View File

@ -184,6 +184,16 @@
Records your session with a small webcam viewer. All recordings are stored locally on your device.
</div>
<div class="webcam-sub-options" id="webcam-sub-options" style="display: none;">
<div class="setting-row">
<label for="webcam-output-directory">Output Directory:</label>
<div class="directory-selector">
<input type="text" id="webcam-output-path" readonly placeholder="Select directory for saving recordings..." style="flex: 1; margin-right: 10px;">
<button type="button" id="select-webcam-directory" class="btn btn-secondary">📁 Browse</button>
</div>
<div class="setting-description">
Choose where session recordings will be automatically saved. Directory will be remembered for future sessions. If no directory is selected, recordings will download automatically.
</div>
</div>
<div class="setting-row">
<label for="webcam-position">Viewer Position:</label>
<select id="webcam-position">
@ -1198,9 +1208,6 @@
<button id="new-settings" class="btn btn-secondary">
⚙️ Change Settings
</button>
<button id="session-videos" class="btn btn-secondary">
📹 Session Videos
</button>
<button id="back-to-home-results" class="btn btn-secondary">
🏠 Back to Home
</button>
@ -1208,37 +1215,7 @@
</div>
</div>
<!-- Session Videos Gallery -->
<div class="session-videos-gallery" id="session-videos-gallery" style="display: none;">
<div class="videos-container">
<div class="videos-header">
<h2>📹 Session Videos</h2>
<p>Your recorded Quick Play sessions</p>
</div>
<div class="videos-content">
<div class="videos-grid" id="videos-grid">
<!-- Videos will be populated here -->
</div>
<div class="no-videos-message" id="no-videos-message" style="display: none;">
<div class="empty-state">
<div class="empty-icon">📹</div>
<h3>No Session Videos Yet</h3>
<p>Enable session recording in Quick Play setup to start recording your training sessions.</p>
</div>
</div>
</div>
<div class="videos-actions">
<button id="clear-all-videos" class="btn btn-danger">
🗑️ Clear All Videos
</button>
<button id="back-to-results" class="btn btn-secondary">
⬅️ Back to Results
</button>
</div>
</div>
</div>
</main>
<!-- Scripts -->
@ -1285,6 +1262,7 @@
videoOpacity: 0.7,
// Webcam recording settings
enableSessionRecording: false,
webcamOutputDirectory: '',
webcamPosition: 'bottom-right',
webcamSize: 'small',
// Task management
@ -1310,13 +1288,41 @@
xp: 0
};
// 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',
'video/mp4;codecs=avc1.64001E',
'video/mp4'
];
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:');
console.error('WebM VP8:', MediaRecorder.isTypeSupported('video/webm;codecs=vp8'));
console.error('WebM VP9:', MediaRecorder.isTypeSupported('video/webm;codecs=vp9'));
console.error('WebM:', MediaRecorder.isTypeSupported('video/webm'));
return false;
}
}
// Initialize Quick Play when page loads
document.addEventListener('DOMContentLoaded', async function() {
console.log('⚡ Initializing Quick Play...');
try {
// Check MP4 support first
checkMP4Support();
// Load saved settings
loadSavedSettings();
await loadSavedSettings();
console.log('✅ Settings loaded');
// Setup event listeners
@ -1411,7 +1417,43 @@
}
});
function loadSavedSettings() {
// Directory handle persistence functions
async function storeDirectoryHandle(directoryHandle) {
// Modern browsers with Origin Private File System API
if ('storage' in navigator && 'persist' in navigator.storage) {
try {
const opfsRoot = await navigator.storage.getDirectory();
const handleFile = await opfsRoot.getFileHandle('webcam-directory-handle', { create: true });
const writable = await handleFile.createWritable();
await writable.write(JSON.stringify({ name: directoryHandle.name }));
await writable.close();
return 'opfs-stored';
} catch (error) {
console.log('OPFS storage failed:', error);
}
}
// Fallback - just return a reference ID
return `handle-${Date.now()}`;
}
async function loadStoredDirectory() {
try {
// Try to load saved directory name
const savedDirectory = localStorage.getItem('webcamRecordingDirectory');
if (savedDirectory) {
quickPlaySettings.webcamOutputDirectory = savedDirectory;
document.getElementById('webcam-output-path').value = savedDirectory;
console.log('📁 Restored recording directory:', savedDirectory);
return true;
}
} catch (error) {
console.error('Error loading stored directory:', error);
}
return false;
}
async function loadSavedSettings() {
try {
const saved = localStorage.getItem('quickPlaySettings');
if (saved) {
@ -1424,6 +1466,10 @@
} else {
console.log('📂 No saved settings found, using defaults');
}
// Also load saved directory
await loadStoredDirectory();
} catch (error) {
console.warn('Failed to load saved settings:', error);
}
@ -2057,37 +2103,7 @@
});
}
// Session videos button
const sessionVideosBtn = document.getElementById('session-videos');
if (sessionVideosBtn) {
sessionVideosBtn.addEventListener('click', (e) => {
e.preventDefault();
console.log('Session videos button clicked');
showSessionVideosGallery();
});
}
// Back to results from videos gallery
const backToResultsBtn = document.getElementById('back-to-results');
if (backToResultsBtn) {
backToResultsBtn.addEventListener('click', (e) => {
e.preventDefault();
console.log('Back to results button clicked');
hideSessionVideosGallery();
});
}
// Clear all videos button
const clearAllVideosBtn = document.getElementById('clear-all-videos');
if (clearAllVideosBtn) {
clearAllVideosBtn.addEventListener('click', (e) => {
e.preventDefault();
console.log('Clear all videos button clicked');
if (confirm('⚠️ Are you sure you want to delete all recorded session videos? This cannot be undone.')) {
clearAllSessionVideos();
}
});
}
// Force exit button
const forceExitBtn = document.getElementById('force-exit');
@ -2235,6 +2251,42 @@
quickPlaySettings.enableSessionRecording = e.target.checked;
updateWebcamOptionsVisibility();
});
document.getElementById('select-webcam-directory').addEventListener('click', async () => {
try {
const directoryHandle = await window.showDirectoryPicker();
const directoryPath = directoryHandle.name;
// Save directory persistently
quickPlaySettings.webcamOutputDirectory = directoryPath;
quickPlaySettings.webcamDirectoryHandle = directoryHandle;
// Save to localStorage for persistence across sessions
localStorage.setItem('selectedVideoDirectory', JSON.stringify({
name: directoryPath,
timestamp: Date.now()
}));
// Also save to more persistent storage
localStorage.setItem('webcamRecordingDirectory', directoryPath);
// Try to store directory handle reference (where supported)
try {
const handleId = await storeDirectoryHandle(directoryHandle);
localStorage.setItem('webcamDirectoryHandleId', handleId);
} catch (handleError) {
console.log('📁 Directory handle storage not supported, will use fallback download');
}
document.getElementById('webcam-output-path').value = directoryPath;
console.log('📁 Webcam output directory selected and saved:', directoryPath);
if (window.flashMessageManager) {
window.flashMessageManager.show(`📁 Recording directory set to: ${directoryPath}`, 'positive');
}
} catch (error) {
console.log('📁 Directory selection cancelled or failed:', error);
}
});
document.getElementById('webcam-position').addEventListener('change', (e) => {
quickPlaySettings.webcamPosition = e.target.value;
});
@ -2354,6 +2406,14 @@
};
console.log('📊 Session stats reset:', sessionStats);
// Start periodic XP display updates (every minute)
if (window.xpDisplayInterval) {
clearInterval(window.xpDisplayInterval);
}
window.xpDisplayInterval = setInterval(() => {
updateSessionDisplay();
}, 60000); // Update every minute
// Initialize webcam recording if enabled
if (quickPlaySettings.enableSessionRecording) {
initializeWebcamRecording();
@ -3034,17 +3094,40 @@
}
}
function awardSessionTimeXP() {
// Award XP for incomplete sessions based on time spent
function calculateCurrentTimeBonus() {
// Calculate current time bonus without awarding it (for display purposes)
const sessionDurationMinutes = (Date.now() - sessionStats.started) / (1000 * 60);
const timeBasedXP = Math.floor(sessionDurationMinutes / 2); // 1 XP per 2 minutes of session time
const complete15MinIntervals = Math.floor(sessionDurationMinutes / 15);
let timeBasedXP = complete15MinIntervals * 2; // 2 XP per complete 15-minute interval
// Add recording bonus if session recording is enabled
if (quickPlaySettings.enableSessionRecording) {
const recordingBonusXP = complete15MinIntervals * 1; // Additional 1 XP per interval when recording
timeBasedXP += recordingBonusXP;
}
return timeBasedXP;
}
function awardSessionTimeXP() {
// Award XP based on new system: 2 XP per complete 15-minute interval
const sessionDurationMinutes = (Date.now() - sessionStats.started) / (1000 * 60);
const complete15MinIntervals = Math.floor(sessionDurationMinutes / 15);
let timeBasedXP = complete15MinIntervals * 2; // 2 XP per complete 15-minute interval
// Add recording bonus if session recording is enabled
if (quickPlaySettings.enableSessionRecording) {
const recordingBonusXP = complete15MinIntervals * 1; // Additional 1 XP per interval when recording
timeBasedXP += recordingBonusXP;
console.log(`📹 Recording bonus: ${recordingBonusXP} XP for ${complete15MinIntervals} complete 15-minute intervals`);
}
if (timeBasedXP > 0) {
sessionStats.xp += timeBasedXP;
if (gameInstance && gameInstance.gameState) {
gameInstance.gameState.xp = (gameInstance.gameState.xp || 0) + timeBasedXP;
}
console.log(`⏰ Awarded ${timeBasedXP} XP for ${sessionDurationMinutes.toFixed(1)} minutes of session time`);
console.log(`⏰ Awarded ${timeBasedXP} XP for ${complete15MinIntervals} complete 15-minute intervals (${sessionDurationMinutes.toFixed(1)} minutes total)`);
}
return timeBasedXP;
@ -3669,6 +3752,13 @@
console.log('endGame called - starting game termination');
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 => {
@ -4072,10 +4162,12 @@
tasksElement.textContent = sessionStats.completed;
}
// Update XP display
// Update XP display (include current time bonus)
const xpElement = document.getElementById('current-xp');
if (xpElement) {
xpElement.textContent = sessionStats.xp;
const currentTimeBonus = calculateCurrentTimeBonus();
const totalCurrentXP = sessionStats.xp + currentTimeBonus;
xpElement.textContent = totalCurrentXP;
}
}
@ -4449,9 +4541,15 @@
console.log('🎥 Initializing webcam recording for session...');
try {
// Request camera access
// Request camera access with high quality 16:9 settings
webcamStream = await navigator.mediaDevices.getUserMedia({
video: true,
video: {
width: { ideal: 1920, min: 1280 },
height: { ideal: 1080, min: 720 },
aspectRatio: { ideal: 16/9 },
frameRate: { ideal: 30, min: 24 },
facingMode: 'user'
},
audio: false // Audio disabled for privacy
});
@ -4464,20 +4562,42 @@
// Apply user settings
webcamViewer.className = `webcam-viewer active ${quickPlaySettings.webcamSize} ${quickPlaySettings.webcamPosition}`;
// Set up recording with lower quality for storage efficiency
// Set up recording - Force MP4 format for Electron
recordedChunks = [];
const options = {
mimeType: 'video/webm;codecs=vp8',
videoBitsPerSecond: 250000 // 250kbps for smaller file size
};
// Try different recording options if not supported
if (MediaRecorder.isTypeSupported('video/webm;codecs=vp8')) {
mediaRecorder = new MediaRecorder(webcamStream, options);
} else if (MediaRecorder.isTypeSupported('video/webm')) {
mediaRecorder = new MediaRecorder(webcamStream, { mimeType: 'video/webm' });
} else {
mediaRecorder = new MediaRecorder(webcamStream);
// Try various MP4 codec combinations for Electron compatibility
const mp4Codecs = [
'video/mp4;codecs=avc1.42E01E', // H.264 Baseline
'video/mp4;codecs=avc1.4D401E', // H.264 Main
'video/mp4;codecs=avc1.64001E', // H.264 High
'video/mp4', // Generic MP4
];
let mediaRecorderCreated = false;
// Try each MP4 codec until one works
for (const codec of mp4Codecs) {
if (MediaRecorder.isTypeSupported(codec)) {
try {
mediaRecorder = new MediaRecorder(webcamStream, {
mimeType: codec,
videoBitsPerSecond: 4000000, // 4 Mbps for high quality 1080p
bitsPerSecond: 4000000 // Fallback for older browsers
});
quickPlaySettings.recordingMimeType = codec;
quickPlaySettings.recordingExtension = 'mp4';
console.log('🎥 Successfully using MP4 codec:', codec);
mediaRecorderCreated = true;
break;
} catch (error) {
console.warn('Failed to create MediaRecorder with codec:', codec, error);
}
}
}
// If no MP4 codec worked, throw error since user wants MP4 only
if (!mediaRecorderCreated) {
throw new Error('❌ MP4 recording not supported in this Electron version. Please update Electron or Chrome engine.');
}
mediaRecorder.ondataavailable = (event) => {
@ -4490,9 +4610,9 @@
saveRecordedSession();
};
// Start recording
mediaRecorder.start();
console.log('✅ Session recording started');
// Start recording with optimized timeslice for quality
mediaRecorder.start(1000); // Collect data every 1 second for better quality
console.log('✅ High-quality session recording started at 4 Mbps');
// Set up viewer controls
setupWebcamViewerControls();
@ -4588,313 +4708,70 @@
console.log('✅ Webcam recording stopped');
}
function saveRecordedSession() {
console.log('💾 Saving recorded session to gallery...');
async function saveRecordedSession() {
console.log('💾 Automatically saving recorded session...');
if (recordedChunks.length === 0) {
console.warn('No recorded data to save');
return;
}
const blob = new Blob(recordedChunks, { type: 'video/webm' });
const reader = new FileReader();
// Use the recording format that was selected during setup
const mimeType = quickPlaySettings.recordingMimeType || 'video/mp4';
const extension = quickPlaySettings.recordingExtension || 'mp4';
const blob = new Blob(recordedChunks, { type: mimeType });
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `quick-play-session-${timestamp}.${extension}`;
reader.onload = function(event) {
// Create video element to capture thumbnail
const video = document.createElement('video');
video.src = event.target.result;
video.muted = true;
video.currentTime = 2; // Seek to 2 seconds for thumbnail
video.addEventListener('seeked', function() {
// Create canvas to capture thumbnail
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 160; // Small thumbnail size
canvas.height = 90; // 16:9 aspect ratio
// Try to save to user-selected directory first (if available)
if (quickPlaySettings.webcamDirectoryHandle) {
try {
const fileHandle = await quickPlaySettings.webcamDirectoryHandle.getFileHandle(filename, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(blob);
await writable.close();
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const thumbnailDataURL = canvas.toDataURL('image/jpeg', 0.7); // Compressed thumbnail
console.log('✅ Session recording saved to directory:', filename);
const videoData = {
id: `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
thumbnail: thumbnailDataURL, // Store only thumbnail, not full video
videoBlob: event.target.result, // Store full video separately for download
timestamp: Date.now(),
sessionType: 'quick-play-recording',
duration: Date.now() - sessionStats.started,
metadata: {
type: 'session-recording',
format: 'webm',
source: 'quick-play',
settings: {
position: quickPlaySettings.webcamPosition,
size: quickPlaySettings.webcamSize
}
}
};
try {
// Try to save - if it fails due to quota, save without full video
let capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
capturedVideos.push(videoData);
// Keep only last 5 recordings to manage storage
if (capturedVideos.length > 5) {
capturedVideos = capturedVideos.slice(-5);
}
localStorage.setItem('capturedVideos', JSON.stringify(capturedVideos));
console.log('✅ Session recording saved with full video:', videoData.id);
} catch (error) {
console.warn('⚠️ Full video too large for storage, saving thumbnail only:', error);
// Fallback: Save without full video data
const lightVideoData = {
...videoData,
videoBlob: null, // Remove full video data
storageError: true
};
try {
let capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
capturedVideos.push(lightVideoData);
if (capturedVideos.length > 10) {
capturedVideos = capturedVideos.slice(-10);
}
localStorage.setItem('capturedVideos', JSON.stringify(capturedVideos));
console.log('✅ Session recording saved (thumbnail only):', lightVideoData.id);
// Offer immediate download since storage failed
if (confirm('📹 Video too large for gallery storage. Download now?')) {
const a = document.createElement('a');
a.href = event.target.result;
a.download = `quick-play-session-${Date.now()}.webm`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
} catch (secondError) {
console.error('❌ Failed to save even thumbnail:', secondError);
// Last resort: offer immediate download
const a = document.createElement('a');
a.href = event.target.result;
a.download = `quick-play-session-${Date.now()}.webm`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
}
// Show completion message
if (window.flashMessageManager) {
window.flashMessageManager.show('📹 Session recording saved! Check library gallery to view.', 'positive');
window.flashMessageManager.show(`📹 Recording saved to directory: ${filename}`, 'positive');
}
// Clean up
recordedChunks = [];
video.remove();
});
video.addEventListener('error', function() {
console.warn('⚠️ Could not create thumbnail, saving without thumbnail');
// Fallback without thumbnail
const videoData = {
id: `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
thumbnail: null,
videoBlob: event.target.result,
timestamp: Date.now(),
sessionType: 'quick-play-recording',
duration: Date.now() - sessionStats.started,
metadata: {
type: 'session-recording',
format: 'webm',
source: 'quick-play',
settings: {
position: quickPlaySettings.webcamPosition,
size: quickPlaySettings.webcamSize
}
}
};
return;
// Same storage logic as above...
recordedChunks = [];
});
};
reader.readAsDataURL(blob);
}
// Session Videos Gallery Functions
function showSessionVideosGallery() {
console.log('📹 Opening session videos gallery');
// Hide results screen, show videos gallery
document.getElementById('quick-play-results').style.display = 'none';
document.getElementById('session-videos-gallery').style.display = 'block';
// Load and display videos
loadSessionVideos();
}
function hideSessionVideosGallery() {
console.log('📹 Closing session videos gallery');
// Hide videos gallery, show results screen
document.getElementById('session-videos-gallery').style.display = 'none';
document.getElementById('quick-play-results').style.display = 'block';
}
function loadSessionVideos() {
const capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
const videosGrid = document.getElementById('videos-grid');
const noVideosMessage = document.getElementById('no-videos-message');
if (capturedVideos.length === 0) {
videosGrid.innerHTML = '';
noVideosMessage.style.display = 'block';
return;
} catch (error) {
console.error('❌ Directory save failed, falling back to download:', error);
}
}
noVideosMessage.style.display = 'none';
// Sort by timestamp (newest first)
capturedVideos.sort((a, b) => b.timestamp - a.timestamp);
videosGrid.innerHTML = capturedVideos.map(video => {
const date = new Date(video.timestamp);
const duration = formatDuration(video.duration);
return `
<div class="video-item" data-video-id="${video.id}">
<div class="video-preview">
<video preload="metadata" muted>
<source src="${video.dataURL}" type="video/webm">
</video>
<div class="video-overlay">
<button class="play-btn" onclick="playVideo('${video.id}')">▶️</button>
</div>
</div>
<div class="video-info">
<div class="video-title">Session Recording</div>
<div class="video-meta">
<span class="video-date">${date.toLocaleDateString()} ${date.toLocaleTimeString()}</span>
<span class="video-duration">${duration}</span>
</div>
<div class="video-settings">
Position: ${video.metadata.settings.position} | Size: ${video.metadata.settings.size}
</div>
</div>
<div class="video-actions">
<button class="btn btn-sm btn-primary" onclick="downloadVideo('${video.id}')">
💾 Download
</button>
<button class="btn btn-sm btn-danger" onclick="deleteVideo('${video.id}')">
🗑️ Delete
</button>
</div>
</div>
`;
}).join('');
}
function formatDuration(ms) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
function playVideo(videoId) {
const capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
const video = capturedVideos.find(v => v.id === videoId);
if (!video) {
console.warn('Video not found:', videoId);
return;
}
// Create fullscreen video player
const overlay = document.createElement('div');
overlay.className = 'video-player-overlay';
overlay.innerHTML = `
<div class="video-player-container">
<video controls autoplay>
<source src="${video.dataURL}" type="video/webm">
</video>
<button class="close-player" onclick="closeVideoPlayer()"></button>
</div>
`;
document.body.appendChild(overlay);
window.currentVideoOverlay = overlay;
}
function closeVideoPlayer() {
if (window.currentVideoOverlay) {
document.body.removeChild(window.currentVideoOverlay);
window.currentVideoOverlay = null;
}
}
function downloadVideo(videoId) {
const capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
const video = capturedVideos.find(v => v.id === videoId);
if (!video) {
console.warn('Video not found:', videoId);
return;
}
// Automatic download fallback (no prompts)
console.log('💾 Auto-downloading recording...');
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = video.dataURL;
a.download = `quick-play-session-${new Date(video.timestamp).toISOString().slice(0, 19)}.webm`;
a.href = url;
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('📹 Video downloaded:', videoId);
console.log('✅ Session recording auto-downloaded:', filename);
if (window.flashMessageManager) {
window.flashMessageManager.show('📹 Video downloaded successfully!', 'positive');
window.flashMessageManager.show(`📹 Recording downloaded: ${filename}`, 'positive');
}
// Clean up
recordedChunks = [];
}
function deleteVideo(videoId) {
if (!confirm('⚠️ Are you sure you want to delete this video? This cannot be undone.')) {
return;
}
let capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
capturedVideos = capturedVideos.filter(v => v.id !== videoId);
localStorage.setItem('capturedVideos', JSON.stringify(capturedVideos));
console.log('🗑️ Video deleted:', videoId);
// Reload the gallery
loadSessionVideos();
if (window.flashMessageManager) {
window.flashMessageManager.show('🗑️ Video deleted successfully!', 'positive');
}
}
function clearAllSessionVideos() {
localStorage.removeItem('capturedVideos');
loadSessionVideos();
console.log('🗑️ All session videos cleared');
if (window.flashMessageManager) {
window.flashMessageManager.show('🗑️ All videos cleared successfully!', 'positive');
}
}
function showErrorDialog(message) {
alert(`❌ Error: ${message}`);
@ -9827,17 +9704,17 @@
/* Size variants */
.webcam-viewer.small {
width: 150px;
height: 113px; /* 4:3 aspect ratio */
height: 84px; /* 16:9 aspect ratio */
}
.webcam-viewer.medium {
width: 200px;
height: 150px;
height: 113px; /* 16:9 aspect ratio */
}
.webcam-viewer.large {
width: 250px;
height: 188px;
height: 141px; /* 16:9 aspect ratio */
}
/* Position variants */

View File

@ -2,6 +2,12 @@ const { app, BrowserWindow, dialog, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs').promises;
// Enable MP4 codec support in Electron
app.commandLine.appendSwitch('enable-features', 'VaapiVideoDecoder');
app.commandLine.appendSwitch('use-file-for-fake-video-capture', 'false');
app.commandLine.appendSwitch('enable-experimental-web-platform-features');
app.commandLine.appendSwitch('enable-blink-features', 'MediaRecorder');
let mainWindow;
// Window state management
@ -70,7 +76,10 @@ async function createWindow() {
contextIsolation: true,
enableRemoteModule: false,
webSecurity: false, // Allow requests to localhost
preload: path.join(__dirname, 'preload.js')
preload: path.join(__dirname, 'preload.js'),
// Enable media features for MP4 recording
allowRunningInsecureContent: true,
experimentalFeatures: true
},
icon: path.join(__dirname, 'assets', 'icon.png'),
show: false,

View File

@ -6949,4 +6949,152 @@ button#start-mirror-btn:disabled {
.character-toggle-btn:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
/* Game Guide Styles */
.game-guide-section {
margin: 1.5rem 0;
text-align: center;
}
.btn-guide {
background: linear-gradient(135deg, #4a148c, #6a1b9a);
color: white;
border: 2px solid #9c27b0;
padding: 0.8rem 2rem;
border-radius: 25px;
font-weight: bold;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(156, 39, 176, 0.3);
}
.btn-guide:hover {
background: linear-gradient(135deg, #6a1b9a, #8e24aa);
border-color: #ba68c8;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(156, 39, 176, 0.4);
}
.game-guide {
max-width: 800px;
margin: 1rem auto;
background: rgba(26, 26, 26, 0.95);
border: 2px solid #9c27b0;
border-radius: 15px;
box-shadow: 0 8px 32px rgba(156, 39, 176, 0.3);
backdrop-filter: blur(10px);
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.guide-content {
padding: 2rem;
text-align: left;
}
.guide-content h3 {
color: #ba68c8;
font-size: 1.5rem;
margin-bottom: 1.5rem;
text-align: center;
text-shadow: 0 0 10px rgba(186, 104, 200, 0.5);
}
.guide-section {
display: grid;
gap: 1rem;
margin-bottom: 2rem;
}
.guide-item {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
background: rgba(156, 39, 176, 0.1);
border: 1px solid rgba(156, 39, 176, 0.3);
border-radius: 10px;
transition: all 0.3s ease;
}
.guide-item:hover {
background: rgba(156, 39, 176, 0.15);
border-color: rgba(156, 39, 176, 0.5);
transform: translateY(-2px);
}
.guide-icon {
font-size: 2rem;
min-width: 60px;
text-align: center;
background: rgba(156, 39, 176, 0.2);
border-radius: 50%;
padding: 0.5rem;
border: 2px solid rgba(156, 39, 176, 0.4);
}
.guide-info h4 {
color: #e1bee7;
font-size: 1.2rem;
margin: 0 0 0.5rem 0;
font-weight: bold;
}
.guide-info p {
color: #cccccc;
line-height: 1.5;
margin: 0;
font-size: 0.95rem;
}
.guide-tips {
background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.3);
border-radius: 10px;
padding: 1.5rem;
}
.guide-tips h4 {
color: #81c784;
font-size: 1.2rem;
margin: 0 0 1rem 0;
text-align: center;
}
.guide-tips ul {
list-style: none;
padding: 0;
margin: 0;
}
.guide-tips li {
color: #cccccc;
padding: 0.5rem 0;
padding-left: 1.5rem;
position: relative;
line-height: 1.4;
}
.guide-tips li::before {
content: "💡";
position: absolute;
left: 0;
top: 0.5rem;
}
.guide-tips strong {
color: #81c784;
font-weight: bold;
}

View File

@ -36,6 +36,49 @@
font-style: italic;
}
/* Scenario Status Bar */
.scenario-status-bar {
display: flex;
justify-content: center;
align-items: center;
background: rgba(155, 89, 182, 0.1);
border: 1px solid #9b59b6;
border-radius: 10px;
margin: 1rem;
padding: 0.8rem;
gap: 2rem;
backdrop-filter: blur(5px);
}
.scenario-status-bar .status-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.scenario-status-bar .status-label {
color: #9b59b6;
font-size: 0.85rem;
font-weight: bold;
margin-bottom: 0.2rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.scenario-status-bar .status-value {
color: #ffffff;
font-size: 1.1rem;
font-weight: bold;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
}
.scenario-status-bar .timer-display {
font-family: 'Courier New', monospace;
color: #00ff88;
text-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
}
.training-controls {
background: rgba(155, 89, 182, 0.1);
border: 1px solid #9b59b6;
@ -888,6 +931,22 @@
<div class="subtitle">Master the Art of Dedicated Gooning</div>
</div>
<!-- Game Status Bar -->
<div class="scenario-status-bar" id="scenario-status-bar" style="display: none;">
<div class="status-item">
<span class="status-label">TIME:</span>
<span id="scenario-timer" class="status-value timer-display">00:00</span>
</div>
<div class="status-item">
<span class="status-label">XP:</span>
<span id="scenario-current-xp" class="status-value">0</span>
</div>
<div class="status-item">
<span class="status-label">ACTIVITY:</span>
<span id="scenario-activity" class="status-value">Ready</span>
</div>
</div>
<!-- Library Status -->
<div class="library-status" id="libraryStatus">
<h3>📚 Library Status</h3>
@ -2288,6 +2347,16 @@
window.game.gameState.startTime = Date.now();
window.game.gameState.xp = 0;
// Initialize scenario XP tracking
if (window.game.initializeScenarioXp) {
window.game.initializeScenarioXp();
console.log('📊 Scenario XP system initialized');
}
// Show status bar and start XP tracking
showScenarioStatusBar();
startScenarioXPTracking();
// Load the first training task directly
if (trainingTasks.length > 0) {
console.log('<27> Loading first training task directly...');
@ -2432,6 +2501,13 @@
window.game.gameState.isRunning = false;
window.game.gameState.isPaused = false;
// Save scenario XP to player stats before clearing
saveScenarioXPToPlayerStats();
// Hide status bar and stop XP tracking
hideScenarioStatusBar();
stopScenarioXPTracking();
// Clear current task
window.game.gameState.currentTask = null;
window.game.currentTask = null;
@ -3469,6 +3545,136 @@
document.body.insertAdjacentHTML('beforeend', galleryHtml);
}
// ===== SCENARIO XP STATUS BAR FUNCTIONS =====
function showScenarioStatusBar() {
const statusBar = document.getElementById('scenario-status-bar');
if (statusBar) {
statusBar.style.display = 'flex';
console.log('📊 Scenario status bar displayed');
}
}
function hideScenarioStatusBar() {
const statusBar = document.getElementById('scenario-status-bar');
if (statusBar) {
statusBar.style.display = 'none';
console.log('📊 Scenario status bar hidden');
}
}
function updateScenarioStatusBar() {
if (!window.game || !window.game.gameState) return;
// Update timer
const timerElement = document.getElementById('scenario-timer');
if (timerElement && window.game.gameState.startTime) {
const elapsed = Date.now() - window.game.gameState.startTime;
const minutes = Math.floor(elapsed / 60000);
const seconds = Math.floor((elapsed % 60000) / 1000);
timerElement.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
// Update XP display
const xpElement = document.getElementById('scenario-current-xp');
if (xpElement) {
const currentXP = getCurrentScenarioXP();
xpElement.textContent = currentXP;
}
// Update activity status
const activityElement = document.getElementById('scenario-activity');
if (activityElement) {
activityElement.textContent = getScenarioActivityStatus();
}
}
function getCurrentScenarioXP() {
if (!window.game || !window.game.gameState || !window.game.gameState.scenarioXp) {
return 0;
}
return window.game.gameState.scenarioXp.total || 0;
}
function getScenarioActivityStatus() {
if (!window.game || !window.game.gameState || !window.game.gameState.scenarioTracking) {
return 'Ready';
}
const tracking = window.game.gameState.scenarioTracking;
if (tracking.isInWebcamActivity) {
return 'Webcam Active';
} else if (tracking.isInFocusActivity) {
return 'Focus Task';
} else if (window.game.gameState.isRunning) {
return 'In Progress';
} else {
return 'Ready';
}
}
function startScenarioXPTracking() {
// Clear any existing interval
if (window.scenarioXPInterval) {
clearInterval(window.scenarioXPInterval);
}
// Update status bar every second and trigger XP calculations
window.scenarioXPInterval = setInterval(() => {
// Manually trigger scenario XP updates (since timer might be overridden)
if (window.game && window.game.gameState && window.game.gameState.isRunning) {
if (window.game.updateScenarioTimeBasedXp) {
window.game.updateScenarioTimeBasedXp();
}
if (window.game.updateScenarioFocusXp) {
window.game.updateScenarioFocusXp();
}
if (window.game.updateScenarioWebcamXp) {
window.game.updateScenarioWebcamXp();
}
}
updateScenarioStatusBar();
}, 1000);
console.log('📊 Started scenario XP tracking');
}
function stopScenarioXPTracking() {
if (window.scenarioXPInterval) {
clearInterval(window.scenarioXPInterval);
window.scenarioXPInterval = null;
console.log('📊 Stopped scenario XP tracking');
}
}
function saveScenarioXPToPlayerStats() {
if (!window.game || !window.game.gameState || !window.game.gameState.scenarioXp) {
console.log('📊 No scenario XP to save');
return;
}
const totalScenarioXP = window.game.gameState.scenarioXp.total || 0;
if (totalScenarioXP > 0) {
// Award XP to player stats system
if (window.playerStats) {
window.playerStats.awardXP(totalScenarioXP, 'scenario');
console.log(`📊 Awarded ${totalScenarioXP} scenario XP to player stats`);
}
// Also add to overall XP counter (for compatibility with existing system)
if (window.game.dataManager) {
const overallXp = window.game.dataManager.get('overallXp') || 0;
const newOverallXp = overallXp + totalScenarioXP;
window.game.dataManager.set('overallXp', newOverallXp);
console.log(`📊 Added ${totalScenarioXP} XP to overall counter: ${overallXp} → ${newOverallXp}`);
}
} else {
console.log('📊 No scenario XP earned this session');
}
}
</script>
</body>
</html>