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>
|
||||
|
||||
|
||||
<!-- 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 -->
|
||||
<div class="setting-group">
|
||||
|
|
@ -1164,12 +1198,47 @@
|
|||
<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>
|
||||
</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>
|
||||
|
||||
<!-- Scripts -->
|
||||
|
|
@ -1214,6 +1283,10 @@
|
|||
enableVideoSound: true,
|
||||
enableVideoControls: false,
|
||||
videoOpacity: 0.7,
|
||||
// Webcam recording settings
|
||||
enableSessionRecording: false,
|
||||
webcamPosition: 'bottom-right',
|
||||
webcamSize: 'small',
|
||||
// Task management
|
||||
disabledTasks: {
|
||||
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() {
|
||||
try {
|
||||
console.log('🎬 Attempting to initialize video library for Quick Play...');
|
||||
|
|
@ -1973,6 +2057,38 @@
|
|||
});
|
||||
}
|
||||
|
||||
// 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');
|
||||
if (forceExitBtn) {
|
||||
|
|
@ -2114,6 +2230,18 @@
|
|||
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
|
||||
document.getElementById('include-standard-tasks').addEventListener('change', (e) => {
|
||||
quickPlaySettings.includeStandardTasks = e.target.checked;
|
||||
|
|
@ -2226,6 +2354,11 @@
|
|||
};
|
||||
console.log('📊 Session stats reset:', sessionStats);
|
||||
|
||||
// Initialize webcam recording if enabled
|
||||
if (quickPlaySettings.enableSessionRecording) {
|
||||
initializeWebcamRecording();
|
||||
}
|
||||
|
||||
// Save current settings
|
||||
localStorage.setItem('quickPlaySettings', JSON.stringify(quickPlaySettings));
|
||||
|
||||
|
|
@ -3961,6 +4094,11 @@
|
|||
|
||||
console.log('📊 Showing results screen with data:', results);
|
||||
|
||||
// Stop webcam recording if active
|
||||
if (quickPlaySettings.enableSessionRecording) {
|
||||
stopWebcamRecording();
|
||||
}
|
||||
|
||||
// Hide game screen, show results
|
||||
document.getElementById('quick-play-game').style.display = 'none';
|
||||
document.getElementById('quick-play-results').style.display = 'block';
|
||||
|
|
@ -4126,6 +4264,11 @@
|
|||
function exitToHome() {
|
||||
console.log('Attempting to exit to home...');
|
||||
|
||||
// Stop webcam recording if active
|
||||
if (quickPlaySettings.enableSessionRecording) {
|
||||
stopWebcamRecording();
|
||||
}
|
||||
|
||||
// Stop any running game
|
||||
if (gameInstance) {
|
||||
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) {
|
||||
alert(`❌ Error: ${message}`);
|
||||
}
|
||||
|
|
@ -8927,6 +9429,351 @@
|
|||
border-color: rgba(255, 193, 7, 0.3);
|
||||
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>
|
||||
|
||||
<!-- 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>
|
||||
</html>
|
||||
|
|
@ -2443,6 +2443,11 @@ class TaskChallengeGame {
|
|||
|
||||
updateScenarioTimeBasedXp() {
|
||||
// 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;
|
||||
|
||||
const now = Date.now();
|
||||
|
|
@ -2460,6 +2465,11 @@ class TaskChallengeGame {
|
|||
|
||||
updateScenarioFocusXp() {
|
||||
// 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;
|
||||
|
||||
const now = Date.now();
|
||||
|
|
@ -2478,6 +2488,11 @@ class TaskChallengeGame {
|
|||
|
||||
updateScenarioWebcamXp() {
|
||||
// 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;
|
||||
|
||||
const now = Date.now();
|
||||
|
|
@ -2496,6 +2511,11 @@ class TaskChallengeGame {
|
|||
|
||||
awardScenarioPhotoXp() {
|
||||
// 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.scenarioTracking.totalPhotosThisSession += 1;
|
||||
this.updateScenarioTotalXp();
|
||||
|
|
@ -2504,6 +2524,11 @@ class TaskChallengeGame {
|
|||
|
||||
awardScenarioStepXp() {
|
||||
// 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.updateScenarioTotalXp();
|
||||
console.log(`🎭 Scenario step XP: +5`);
|
||||
|
|
|
|||
|
|
@ -213,7 +213,15 @@ class WebcamManager {
|
|||
|
||||
// Check if session is complete
|
||||
if (photosTaken >= photosNeeded) {
|
||||
// Show completion button
|
||||
// For single-photo sessions (like Quick Play), auto-complete
|
||||
if (photosNeeded === 1) {
|
||||
console.log(`✅ Single-photo session complete! Auto-completing...`);
|
||||
// Small delay to let user see the completion message
|
||||
setTimeout(() => {
|
||||
this.completePhotoSession();
|
||||
}, 1000);
|
||||
} else {
|
||||
// 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';
|
||||
|
|
@ -222,6 +230,7 @@ class WebcamManager {
|
|||
document.querySelector('.session-instruction').textContent = '🎉 Session complete! All photos taken.';
|
||||
|
||||
console.log(`✅ Photo session complete! ${photosTaken}/${photosNeeded} photos taken`);
|
||||
}
|
||||
} else {
|
||||
// Return to camera view for more photos
|
||||
this.showCameraPreview();
|
||||
|
|
|
|||
Loading…
Reference in New Issue