ADD: Session recording with gallery system

ADDED: Complete session recording functionality
- Session recording option in Quick Play setup screen with position/size controls
- Automatic recording initialization when enabled during session startup
- Records entire gameplay session using MediaRecorder API (WebM format)
- Privacy-focused: audio disabled, all recordings stored locally only

IMPLEMENTED: Session Videos Gallery
- Gallery accessible from Quick Play results screen via 'Session Videos' button
- Grid view showing video previews with metadata (date, duration, settings)
- Individual video actions: play fullscreen, download, delete
- Bulk actions: clear all videos with confirmation dialog
- Automatic storage management (keeps last 10 recordings)

FEATURES: Advanced video management
- Fullscreen video player with controls for reviewing sessions
- Smart filename generation with timestamps for downloads
- Video previews with hover-to-play overlay effects
- Responsive grid layout with professional styling
- localStorage-based gallery system matching photo capture pattern

RESULT: Complete session documentation system
- Users can record their training sessions for later review
- Gallery provides easy access to download or manage recordings
- Seamless integration with existing Quick Play workflow
- No external dependencies - fully self-contained recording system
This commit is contained in:
dilgenfritz 2025-11-10 10:38:18 -06:00
parent c93be4416e
commit 6381c292e0
3 changed files with 891 additions and 10 deletions

View File

@ -169,7 +169,41 @@
</div> </div>
</div> </div>
<!-- Webcam Recording Settings -->
<div class="setting-group">
<h3>📹 Webcam Recording</h3>
<div class="webcam-settings">
<div class="webcam-option">
<input type="checkbox" id="enable-session-recording">
<label for="enable-session-recording">
<span class="option-icon">🎥</span>
<span class="option-text">Enable Session Recording</span>
</label>
</div>
<div class="setting-description">
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-position">Viewer Position:</label>
<select id="webcam-position">
<option value="bottom-right" selected>Bottom Right</option>
<option value="bottom-left">Bottom Left</option>
<option value="top-right">Top Right</option>
<option value="top-left">Top Left</option>
</select>
</div>
<div class="setting-row">
<label for="webcam-size">Viewer Size:</label>
<select id="webcam-size">
<option value="small" selected>Small (150px)</option>
<option value="medium">Medium (200px)</option>
<option value="large">Large (250px)</option>
</select>
</div>
</div>
</div>
</div>
<!-- Consequence Settings --> <!-- Consequence Settings -->
<div class="setting-group"> <div class="setting-group">
@ -1164,12 +1198,47 @@
<button id="new-settings" class="btn btn-secondary"> <button id="new-settings" class="btn btn-secondary">
⚙️ Change Settings ⚙️ Change Settings
</button> </button>
<button id="session-videos" class="btn btn-secondary">
📹 Session Videos
</button>
<button id="back-to-home-results" class="btn btn-secondary"> <button id="back-to-home-results" class="btn btn-secondary">
🏠 Back to Home 🏠 Back to Home
</button> </button>
</div> </div>
</div> </div>
</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> </main>
<!-- Scripts --> <!-- Scripts -->
@ -1214,6 +1283,10 @@
enableVideoSound: true, enableVideoSound: true,
enableVideoControls: false, enableVideoControls: false,
videoOpacity: 0.7, videoOpacity: 0.7,
// Webcam recording settings
enableSessionRecording: false,
webcamPosition: 'bottom-right',
webcamSize: 'small',
// Task management // Task management
disabledTasks: { disabledTasks: {
main: [], main: [],
@ -1735,6 +1808,17 @@
} }
} }
function updateWebcamOptionsVisibility() {
const webcamSubOptions = document.getElementById('webcam-sub-options');
const enableRecording = document.getElementById('enable-session-recording').checked;
if (enableRecording) {
webcamSubOptions.style.display = 'block';
} else {
webcamSubOptions.style.display = 'none';
}
}
async function initializeVideoLibrary() { async function initializeVideoLibrary() {
try { try {
console.log('🎬 Attempting to initialize video library for Quick Play...'); console.log('🎬 Attempting to initialize video library for Quick Play...');
@ -1972,6 +2056,38 @@
exitToHome(); exitToHome();
}); });
} }
// 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 // Force exit button
const forceExitBtn = document.getElementById('force-exit'); const forceExitBtn = document.getElementById('force-exit');
@ -2114,6 +2230,18 @@
quickPlaySettings.videoOpacity = parseFloat(e.target.value); quickPlaySettings.videoOpacity = parseFloat(e.target.value);
}); });
// Webcam recording settings
document.getElementById('enable-session-recording').addEventListener('change', (e) => {
quickPlaySettings.enableSessionRecording = e.target.checked;
updateWebcamOptionsVisibility();
});
document.getElementById('webcam-position').addEventListener('change', (e) => {
quickPlaySettings.webcamPosition = e.target.value;
});
document.getElementById('webcam-size').addEventListener('change', (e) => {
quickPlaySettings.webcamSize = e.target.value;
});
// Task type checkboxes // Task type checkboxes
document.getElementById('include-standard-tasks').addEventListener('change', (e) => { document.getElementById('include-standard-tasks').addEventListener('change', (e) => {
quickPlaySettings.includeStandardTasks = e.target.checked; quickPlaySettings.includeStandardTasks = e.target.checked;
@ -2226,6 +2354,11 @@
}; };
console.log('📊 Session stats reset:', sessionStats); console.log('📊 Session stats reset:', sessionStats);
// Initialize webcam recording if enabled
if (quickPlaySettings.enableSessionRecording) {
initializeWebcamRecording();
}
// Save current settings // Save current settings
localStorage.setItem('quickPlaySettings', JSON.stringify(quickPlaySettings)); localStorage.setItem('quickPlaySettings', JSON.stringify(quickPlaySettings));
@ -3961,6 +4094,11 @@
console.log('📊 Showing results screen with data:', results); console.log('📊 Showing results screen with data:', results);
// Stop webcam recording if active
if (quickPlaySettings.enableSessionRecording) {
stopWebcamRecording();
}
// Hide game screen, show results // Hide game screen, show results
document.getElementById('quick-play-game').style.display = 'none'; document.getElementById('quick-play-game').style.display = 'none';
document.getElementById('quick-play-results').style.display = 'block'; document.getElementById('quick-play-results').style.display = 'block';
@ -4126,6 +4264,11 @@
function exitToHome() { function exitToHome() {
console.log('Attempting to exit to home...'); console.log('Attempting to exit to home...');
// Stop webcam recording if active
if (quickPlaySettings.enableSessionRecording) {
stopWebcamRecording();
}
// Stop any running game // Stop any running game
if (gameInstance) { if (gameInstance) {
try { try {
@ -4297,6 +4440,365 @@
} }
} }
// Webcam Recording Functions
let mediaRecorder = null;
let recordedChunks = [];
let webcamStream = null;
async function initializeWebcamRecording() {
console.log('🎥 Initializing webcam recording for session...');
try {
// Request camera access
webcamStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false // Audio disabled for privacy
});
// Set up webcam viewer
const webcamViewer = document.getElementById('webcam-viewer');
const webcamPreview = document.getElementById('webcam-preview');
webcamPreview.srcObject = webcamStream;
// Apply user settings
webcamViewer.className = `webcam-viewer active ${quickPlaySettings.webcamSize} ${quickPlaySettings.webcamPosition}`;
// Set up recording
recordedChunks = [];
mediaRecorder = new MediaRecorder(webcamStream, {
mimeType: 'video/webm'
});
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
saveRecordedSession();
};
// Start recording
mediaRecorder.start();
console.log('✅ Session recording started');
// Set up viewer controls
setupWebcamViewerControls();
} catch (error) {
console.error('❌ Failed to initialize webcam recording:', error);
// Don't show error to user - recording is optional
// Hide the viewer if it failed
const webcamViewer = document.getElementById('webcam-viewer');
webcamViewer.style.display = 'none';
}
}
function setupWebcamViewerControls() {
const toggleBtn = document.getElementById('toggle-webcam-viewer');
const stopBtn = document.getElementById('stop-recording');
const webcamViewer = document.getElementById('webcam-viewer');
// Toggle visibility
toggleBtn.addEventListener('click', () => {
const preview = document.getElementById('webcam-preview');
if (preview.style.display === 'none') {
preview.style.display = 'block';
toggleBtn.textContent = '👁️';
toggleBtn.title = 'Hide Webcam';
} else {
preview.style.display = 'none';
toggleBtn.textContent = '👁️‍🗨️';
toggleBtn.title = 'Show Webcam';
}
});
// Stop recording
stopBtn.addEventListener('click', () => {
stopWebcamRecording();
});
// Make viewer draggable
makeWebcamViewerDraggable();
}
function makeWebcamViewerDraggable() {
const webcamViewer = document.getElementById('webcam-viewer');
let isDragging = false;
let startX, startY, startLeft, startTop;
webcamViewer.addEventListener('mousedown', (e) => {
// Only drag when clicking on the viewer itself, not controls
if (e.target.classList.contains('control-btn')) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = webcamViewer.offsetLeft;
startTop = webcamViewer.offsetTop;
webcamViewer.style.position = 'fixed';
webcamViewer.style.left = startLeft + 'px';
webcamViewer.style.top = startTop + 'px';
webcamViewer.classList.remove('bottom-right', 'bottom-left', 'top-right', 'top-left');
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
webcamViewer.style.left = (startLeft + deltaX) + 'px';
webcamViewer.style.top = (startTop + deltaY) + 'px';
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
}
function stopWebcamRecording() {
console.log('⏹️ Stopping webcam recording...');
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
}
if (webcamStream) {
webcamStream.getTracks().forEach(track => track.stop());
webcamStream = null;
}
const webcamViewer = document.getElementById('webcam-viewer');
webcamViewer.classList.remove('active');
console.log('✅ Webcam recording stopped');
}
function saveRecordedSession() {
console.log('💾 Saving recorded session to gallery...');
if (recordedChunks.length === 0) {
console.warn('No recorded data to save');
return;
}
const blob = new Blob(recordedChunks, { type: 'video/webm' });
const reader = new FileReader();
reader.onload = function(event) {
const videoData = {
id: `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
dataURL: event.target.result,
timestamp: Date.now(),
sessionType: 'quick-play-recording',
duration: Date.now() - sessionStats.started, // Session duration
metadata: {
type: 'session-recording',
format: 'webm',
source: 'quick-play',
settings: {
position: quickPlaySettings.webcamPosition,
size: quickPlaySettings.webcamSize
}
}
};
// Save to localStorage gallery
let savedVideos = JSON.parse(localStorage.getItem('savedSessionVideos') || '[]');
savedVideos.push(videoData);
// Keep only last 10 recordings to manage storage
if (savedVideos.length > 10) {
savedVideos = savedVideos.slice(-10);
}
localStorage.setItem('savedSessionVideos', JSON.stringify(savedVideos));
console.log('✅ Session recording saved to gallery:', videoData.id);
// Show completion message
if (window.flashMessageManager) {
window.flashMessageManager.show('📹 Session recording saved to gallery! Check your session videos to download.', 'positive');
}
// Clean up
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 savedVideos = JSON.parse(localStorage.getItem('savedSessionVideos') || '[]');
const videosGrid = document.getElementById('videos-grid');
const noVideosMessage = document.getElementById('no-videos-message');
if (savedVideos.length === 0) {
videosGrid.innerHTML = '';
noVideosMessage.style.display = 'block';
return;
}
noVideosMessage.style.display = 'none';
// Sort by timestamp (newest first)
savedVideos.sort((a, b) => b.timestamp - a.timestamp);
videosGrid.innerHTML = savedVideos.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 savedVideos = JSON.parse(localStorage.getItem('savedSessionVideos') || '[]');
const video = savedVideos.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 savedVideos = JSON.parse(localStorage.getItem('savedSessionVideos') || '[]');
const video = savedVideos.find(v => v.id === videoId);
if (!video) {
console.warn('Video not found:', videoId);
return;
}
const a = document.createElement('a');
a.href = video.dataURL;
a.download = `quick-play-session-${new Date(video.timestamp).toISOString().slice(0, 19)}.webm`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
console.log('📹 Video downloaded:', videoId);
if (window.flashMessageManager) {
window.flashMessageManager.show('📹 Video downloaded successfully!', 'positive');
}
}
function deleteVideo(videoId) {
if (!confirm('⚠️ Are you sure you want to delete this video? This cannot be undone.')) {
return;
}
let savedVideos = JSON.parse(localStorage.getItem('savedSessionVideos') || '[]');
savedVideos = savedVideos.filter(v => v.id !== videoId);
localStorage.setItem('savedSessionVideos', JSON.stringify(savedVideos));
console.log('🗑️ Video deleted:', videoId);
// Reload the gallery
loadSessionVideos();
if (window.flashMessageManager) {
window.flashMessageManager.show('🗑️ Video deleted successfully!', 'positive');
}
}
function clearAllSessionVideos() {
localStorage.removeItem('savedSessionVideos');
loadSessionVideos();
console.log('🗑️ All session videos cleared');
if (window.flashMessageManager) {
window.flashMessageManager.show('🗑️ All videos cleared successfully!', 'positive');
}
}
function showErrorDialog(message) { function showErrorDialog(message) {
alert(`❌ Error: ${message}`); alert(`❌ Error: ${message}`);
} }
@ -8927,6 +9429,351 @@
border-color: rgba(255, 193, 7, 0.3); border-color: rgba(255, 193, 7, 0.3);
background: rgba(255, 193, 7, 0.05); background: rgba(255, 193, 7, 0.05);
} }
/* Session Videos Gallery Styles */
.session-videos-gallery {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
overflow-y: auto;
z-index: 100;
}
.videos-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.videos-header {
text-align: center;
margin-bottom: 30px;
}
.videos-header h2 {
color: #ffffff;
font-size: 2rem;
margin-bottom: 10px;
}
.videos-header p {
color: #b8b8b8;
font-size: 1.1rem;
}
.videos-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.video-item {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 15px;
padding: 15px;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.video-item:hover {
transform: translateY(-5px);
border-color: rgba(0, 255, 255, 0.5);
box-shadow: 0 10px 30px rgba(0, 255, 255, 0.2);
}
.video-preview {
position: relative;
width: 100%;
height: 200px;
border-radius: 10px;
overflow: hidden;
margin-bottom: 15px;
background: #000;
}
.video-preview video {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.video-preview:hover .video-overlay {
opacity: 1;
}
.play-btn {
background: rgba(0, 255, 255, 0.9);
border: none;
border-radius: 50%;
width: 60px;
height: 60px;
font-size: 1.2rem;
color: #000;
cursor: pointer;
transition: all 0.3s ease;
}
.play-btn:hover {
background: rgba(0, 255, 255, 1);
transform: scale(1.1);
}
.video-info {
margin-bottom: 15px;
}
.video-title {
color: #ffffff;
font-size: 1.1rem;
font-weight: bold;
margin-bottom: 5px;
}
.video-meta {
color: #b8b8b8;
font-size: 0.9rem;
margin-bottom: 5px;
}
.video-meta span {
margin-right: 15px;
}
.video-settings {
color: #888;
font-size: 0.8rem;
}
.video-actions {
display: flex;
gap: 10px;
}
.video-actions .btn {
flex: 1;
padding: 8px 12px;
font-size: 0.9rem;
}
.videos-actions {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 30px;
}
.no-videos-message {
text-align: center;
padding: 60px 20px;
}
.empty-state {
max-width: 400px;
margin: 0 auto;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 20px;
}
.empty-state h3 {
color: #ffffff;
font-size: 1.5rem;
margin-bottom: 15px;
}
.empty-state p {
color: #b8b8b8;
line-height: 1.6;
}
/* Video Player Overlay */
.video-player-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: rgba(0, 0, 0, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.video-player-container {
position: relative;
max-width: 90vw;
max-height: 90vh;
}
.video-player-container video {
width: 100%;
height: auto;
max-height: 90vh;
border-radius: 10px;
}
.close-player {
position: absolute;
top: -40px;
right: 0;
background: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 50%;
width: 35px;
height: 35px;
color: #fff;
font-size: 1.2rem;
cursor: pointer;
transition: all 0.3s ease;
}
.close-player:hover {
background: rgba(255, 255, 255, 0.4);
transform: scale(1.1);
}
/* Webcam Viewer Styles */
.webcam-viewer {
position: fixed;
z-index: 1000;
border: 2px solid rgba(0, 255, 255, 0.6);
border-radius: 10px;
background: rgba(0, 20, 40, 0.9);
backdrop-filter: blur(5px);
box-shadow: 0 4px 20px rgba(0, 255, 255, 0.3);
transition: all 0.3s ease;
cursor: move;
display: none;
overflow: hidden;
}
.webcam-viewer.active {
display: block;
}
.webcam-viewer video {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
.webcam-viewer .viewer-controls {
position: absolute;
top: 5px;
right: 5px;
display: flex;
gap: 5px;
opacity: 0.7;
transition: opacity 0.3s;
}
.webcam-viewer:hover .viewer-controls {
opacity: 1;
}
.webcam-viewer .control-btn {
background: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
border-radius: 3px;
padding: 2px 5px;
font-size: 10px;
cursor: pointer;
transition: background 0.3s;
}
.webcam-viewer .control-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.webcam-viewer .recording-indicator {
position: absolute;
top: 5px;
left: 5px;
background: #ff4444;
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 10px;
font-weight: bold;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Size variants */
.webcam-viewer.small {
width: 150px;
height: 113px; /* 4:3 aspect ratio */
}
.webcam-viewer.medium {
width: 200px;
height: 150px;
}
.webcam-viewer.large {
width: 250px;
height: 188px;
}
/* Position variants */
.webcam-viewer.bottom-right {
bottom: 20px;
right: 20px;
}
.webcam-viewer.bottom-left {
bottom: 20px;
left: 20px;
}
.webcam-viewer.top-right {
top: 20px;
right: 20px;
}
.webcam-viewer.top-left {
top: 20px;
left: 20px;
}
</style> </style>
<!-- Floating Webcam Viewer -->
<div id="webcam-viewer" class="webcam-viewer">
<div class="recording-indicator">● REC</div>
<video id="webcam-preview" autoplay muted></video>
<div class="viewer-controls">
<button class="control-btn" id="toggle-webcam-viewer" title="Hide Webcam">👁️</button>
<button class="control-btn" id="stop-recording" title="Stop Recording">⏹️</button>
</div>
</div>
</body> </body>
</html> </html>

View File

@ -2443,6 +2443,11 @@ class TaskChallengeGame {
updateScenarioTimeBasedXp() { updateScenarioTimeBasedXp() {
// Award 1 XP per 2 minutes of scenario gameplay (only when not paused) // Award 1 XP per 2 minutes of scenario gameplay (only when not paused)
// Initialize scenarioXp if not already done (for Quick Play compatibility)
if (!this.gameState.scenarioXp) {
this.initializeScenarioXp();
}
if (!this.gameState.scenarioTracking.startTime || this.gameState.isPaused) return; if (!this.gameState.scenarioTracking.startTime || this.gameState.isPaused) return;
const now = Date.now(); const now = Date.now();
@ -2460,6 +2465,11 @@ class TaskChallengeGame {
updateScenarioFocusXp() { updateScenarioFocusXp() {
// Award 3 XP per 30 seconds during focus activities // Award 3 XP per 30 seconds during focus activities
// Initialize scenarioXp if not already done (for Quick Play compatibility)
if (!this.gameState.scenarioXp) {
this.initializeScenarioXp();
}
if (!this.gameState.scenarioTracking.isInFocusActivity || this.gameState.isPaused) return; if (!this.gameState.scenarioTracking.isInFocusActivity || this.gameState.isPaused) return;
const now = Date.now(); const now = Date.now();
@ -2478,6 +2488,11 @@ class TaskChallengeGame {
updateScenarioWebcamXp() { updateScenarioWebcamXp() {
// Award XP for webcam mirror activity (x2 multiplier = 1 XP per minute) // Award XP for webcam mirror activity (x2 multiplier = 1 XP per minute)
// Initialize scenarioXp if not already done (for Quick Play compatibility)
if (!this.gameState.scenarioXp) {
this.initializeScenarioXp();
}
if (!this.gameState.scenarioTracking.isInWebcamActivity || this.gameState.isPaused) return; if (!this.gameState.scenarioTracking.isInWebcamActivity || this.gameState.isPaused) return;
const now = Date.now(); const now = Date.now();
@ -2496,6 +2511,11 @@ class TaskChallengeGame {
awardScenarioPhotoXp() { awardScenarioPhotoXp() {
// Award 2 XP per photo taken during scenarios (ROADMAP requirement) // Award 2 XP per photo taken during scenarios (ROADMAP requirement)
// Initialize scenarioXp if not already done (for Quick Play compatibility)
if (!this.gameState.scenarioXp) {
this.initializeScenarioXp();
}
this.gameState.scenarioXp.photoRewards += 2; this.gameState.scenarioXp.photoRewards += 2;
this.gameState.scenarioTracking.totalPhotosThisSession += 1; this.gameState.scenarioTracking.totalPhotosThisSession += 1;
this.updateScenarioTotalXp(); this.updateScenarioTotalXp();
@ -2504,6 +2524,11 @@ class TaskChallengeGame {
awardScenarioStepXp() { awardScenarioStepXp() {
// Award 5 XP per scenario step completion (existing system) // Award 5 XP per scenario step completion (existing system)
// Initialize scenarioXp if not already done (for Quick Play compatibility)
if (!this.gameState.scenarioXp) {
this.initializeScenarioXp();
}
this.gameState.scenarioXp.stepCompletion += 5; this.gameState.scenarioXp.stepCompletion += 5;
this.updateScenarioTotalXp(); this.updateScenarioTotalXp();
console.log(`🎭 Scenario step XP: +5`); console.log(`🎭 Scenario step XP: +5`);

View File

@ -213,15 +213,24 @@ class WebcamManager {
// Check if session is complete // Check if session is complete
if (photosTaken >= photosNeeded) { if (photosTaken >= photosNeeded) {
// Show completion button // For single-photo sessions (like Quick Play), auto-complete
document.getElementById('complete-session').style.display = 'inline-block'; if (photosNeeded === 1) {
document.getElementById('capture-photo').style.display = 'none'; console.log(`✅ Single-photo session complete! Auto-completing...`);
document.getElementById('accept-photo').style.display = 'none'; // Small delay to let user see the completion message
setTimeout(() => {
// Update header this.completePhotoSession();
document.querySelector('.session-instruction').textContent = '🎉 Session complete! All photos taken.'; }, 1000);
} else {
console.log(`✅ Photo session complete! ${photosTaken}/${photosNeeded} photos taken`); // Show completion button for multi-photo sessions
document.getElementById('complete-session').style.display = 'inline-block';
document.getElementById('capture-photo').style.display = 'none';
document.getElementById('accept-photo').style.display = 'none';
// Update header
document.querySelector('.session-instruction').textContent = '🎉 Session complete! All photos taken.';
console.log(`✅ Photo session complete! ${photosTaken}/${photosNeeded} photos taken`);
}
} else { } else {
// Return to camera view for more photos // Return to camera view for more photos
this.showCameraPreview(); this.showCameraPreview();