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:
parent
c93be4416e
commit
6381c292e0
849
quick-play.html
849
quick-play.html
|
|
@ -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>
|
||||||
|
|
@ -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`);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue